Сетка BlockFirst
Картинка BlockFirst
Link
Иконка копировать
Иконка скопированно

Staking and rewards in SushiSwap MasterChef

This article explains how the MasterChef system in SushiSwap operates, managing staking and reward distribution. It covers key mechanisms for token allocation, user interactions with smart contracts, and the principles of earning rewards for participating in liquidity pools.

29.06.2025
Начинающий уровень
Автор BlockFirst
Share
Иконка поделиться
Link
Иконка копировать
Иконка скопировано
ARTICLE BELOW
Картинка
Превью BlockFirst

StakerARewards =
(BlockRewardsOn0to10 * StakerAShareOn0to10) +
(BlockRewardsOn10to15 * StakerAShareOn10to15) +
(BlockRewardsOn15to25 * StakerAShareOn15to25) +
(BlockRewardsOn25to30 * StakerAShareOn25to30)

StakerARewards = 14

it.only("Harvest rewards according with the staker pool's share", async function ()
{ // Arrange Pool
const stakeToken = rewardToken;
await stakeToken.transfer( account2.address, ethers.utils.parseEther("200000") // 200.000 );
await createStakingPool(stakingManager, stakeToken.address);
const amount1 = ethers.utils.parseEther("80");
const amount2 = ethers.utils.parseEther("20");

// Arrange Account1 staking await stakeToken.approve(stakingManager.address, amount1);
await stakingManager.deposit(0, amount1);

// Arrange Account 2 staking await stakeToken.connect(account2).approve(stakingManager.address, amount2);
await stakingManager.connect(account2).deposit(0, amount2);

// Act const acc1HarvestTransaction = await stakingManager.harvestRewards(0);
const acc2HarvestTransaction = await stakingManager .connect(account2) .harvestRewards(0);

// Assert // 2 blocks with 100% participation = 4 reward tokens * 2 blocks = 8 // 1 block with 80% participation = 3.2 reward tokens * 1 block = 3.2 // Account1 Total = 8 + 3.2 = 11.2 reward tokens const expectedAccount1Rewards = ethers.utils.parseEther("11.2");

await expect(acc1HarvestTransaction) .to.emit(stakingManager, "HarvestRewards") .withArgs(account1.address, 0, expectedAccount1Rewards); // 2 block with 20% participation = 0.8 reward tokens * 2 block // Account 1 Total = 1.6 reward tokens const expectedAccount2Rewards = ethers.utils.parseEther("1.6");

await expect(acc2HarvestTransaction) .to.emit(stakingManager, "HarvestRewards") .withArgs(account2.address, 0, expectedAccount2Rewards); });

StakerATokens = 100
AccumulatedRewardsPerShare = (10 / 100) + (5 / 500) + (10 / 500) + (5 / 500)
AccumulatedRewardsPerShare = (10 / 100) + (5 / 500)

StakerARewardsOn15to30 = StakerATokens *
(AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)
StakerARewardsOn15to30 = 100 * ((10 / 500) + (5 / 500))
StakerARewardsOn15to30 = 3

Exactly. This means that if we store the value of AccumulatedRewardsPerShare multiplied by the staker's token amount each time they make a deposit or withdraw, we can simply subtract this value from their total rewards to determine their current earnings. In the MasterChef contract, this is called rewardDebt. It's similar to calculating the total rewards a staker has accumulated since block 0, but with the subtraction of rewards they've already collected or were not entitled to because they hadn't participated in staking yet.

We can also substitute some values to verify whether this approach actually works.

StakerBRewards = StakerBTokens *
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)

And this is very similar to the formula we obtained earlier for StakerBRewards.

StakerARewardsOn15to30 = StakerATokens *
(AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)

Now, you may have noticed that we can once again isolate StakerATokens.

StakerARewardsOn15to30 = StakerATokens *
AccumulatedRewardsPerShare - StakerATokens *
AccumulatedRewardsPerShareOn0to15

And replace StakerARewardsOn0to15 in the previous formula.

StakerARewardsOn0to15 = StakerATokens *
AccumulatedRewardsPerShareOn0to15

Now, we can use the following formula for blocks 0 to 15.

StakerARewardsOn15to30 = StakerATokens *
AccumulatedRewardsPerShare - StakerARewardsOn0to15

We obtain

StakerARewardsOn15to30 = StakerARewards - StakerARewardsOn0to15
StakerARewards = StakerATokens * AccumulatedRewardsPerShare

If we isolate StakerARewardsOn15to30 in the first formula and replace StakerATokens with the value from the second formula, then

StakerARewards = StakerARewardsOn0to15 + StakerARewardsOn15to30
StakerARewards = StakerATokens * AccumulatedRewardsPerShare

StakerBRewards = StakerBTokens *
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)

This is important because although we can use AccumulatedRewardsPerShare to calculate each staker's rewards, we need to subtract the RewardsPerShare that accumulated before their deposit or reward claim. Let's find out how much Staker A earned during their first reward claim, using what we've learned so far.

Since AccumulatedRewardsPerShare is the same for all stakers, we can say that StakerBRewards equals this value minus the rewards they did not receive for blocks 0 to 10.

StakerARewards = StakerATokens *
AccumulatedRewardsPerShare

Then, we can say that StakerARewards equals the product of StakerATokens and AccumulatedRewardsPerShare.

AccumulatedRewardsPerShare =
RewardsPerShareOn0to10 +
RewardsPerShareOn10to15 +
RewardsPerShareOn15to25 +
RewardsPerShareOn25to30

And instead of accSushiPerShare, we will refer to them as the sum: AccumulatedRewardsPerShare.

RewardsPerShareOn0to10 = (10 / 100)
RewardsPerShareOn10to15 = (5 / 500)
RewardsPerShareOn15to25 = (10 / 500)
RewardsPerShareOn25to30 = (5 / 500)

(5 / 500) + (10 / 500) + (5 / 500)

The SushiSwap contract refers to this sum as accSushiPerShare, so let's call each division RewardsPerShare.

StakerBRewards = 16

StakerBRewards =
400 * ( (5 / 500) +
(10 / 500) +
(5 / 500)
)

StakerBRewards =
StakerBTokens * ( (BlockRewardsOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 / TotalTokensOn25to30)
)

StakerBRewards =
(BlockRewardsOn10to15 * StakerBTokens / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerBTokens / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerBTokens / TotalTokensOn25to30)

Now that the rewards for both stakers match what we observed earlier, let's see what can be reused in calculating each one's rewards. As you can see, both formulas for the stakers' rewards contain a common sum in the denominator.

Which aligns with our expectations. Let's do the same for Staker B.

StakerARewards = 100 * (
(10 / 100) +
(5 / 500) +
(10 / 500) +
(5 / 500)
)

We can verify that this works in our scenario by replacing these complex terms with numbers and calculating the total rewards for Staker A.

StakerARewards =
StakerATokens * ( (BlockRewardsOn0to10 / TotalTokensOn0to10) +
(BlockRewardsOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 / TotalTokensOn25to30)
)

And, by isolating StakerATokens, we arrive at the following.

StakerARewards =
(BlockRewardsOn0to10 * StakerATokens / TotalTokensOn0to10) +
(BlockRewardsOn10to15 * StakerATokens / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerATokens / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerATokens / TotalTokensOn25to30)

Then, we can simplify our formula for StakerARewards.

StakerATokensOn0to10 =
StakerATokensOn10to15 =
StakerATokensOn15to25 =
StakerATokensOn25to30 =
StakerATokens

But in this case, the staker had the same amount of tokens contributed across all ranges.

StakerARewards =
(BlockRewardsOn0to10 * StakerATokensOn0to10 / TotalTokensOn0to10) +
(BlockRewardsOn10to15 * StakerATokensOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerATokensOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerATokensOn25to30 / TotalTokensOn25to30)

We have this

StakerAShareOnNtoM = StakerATokensOnNtoM / TotalTokensOnNtoM

StakerANtoMRewards = BlockRewardsOnNtoM * StakerAShareOnNtoM

StakerARewards =
StakerA0to10Rewards +
StakerA10to15Rewards +
StakerA15to25Rewards +
StakerA25to30Rewards

From Block 25 to 30:
BlocksPassed: 5
BlockRewards: $5
StakerATokens: $100
StakerBTokens: $400
TotalTokens: $500
StakerAAccumulatedRewards: $1 + $2
StakerBAccumulatedRewards: $4

From Block 15 to 25:
BlocksPassed: 10
BlockRewards: $10
StakerATokens: $100
StakerBTokens: $400
TotalTokens: $500
StakerAAccumulatedRewards: $2
StakerBAccumulatedRewards: $8 + $4

From Block 10 to 15:
BlocksPassed: 5
BlockRewards = BlocksPassed * RewardsPerBlock
BlockRewards = $5
StakerATokens: $100
StakerBTokens: $400
TotalTokens: $500

StakerAShare = StakerATokens / TotalTokens
StakerAShare = 1/5
StakerAAccumulatedRewards = (BlockRewards * StakerAShare) + StakerAAccumulatedRewards
StakerAAccumulatedRewards = $1 + $10

StakerBShare = StakerBTokens / TotalTokens
StakerBShare = 4/5
StakerBAccumulatedRewards = BlockRewards * StakerBShare
StakerBAccumulatedRewards = $4

From block 0 to 10:
BlocksPassed: 10
BlockRewards = BlocksPassed * RewardsPerBlock
BlockRewards = $10
StakerATokens:
$100 TotalTokens: $100

StakerAShare = StakerATokens / TotalTokens
StakerAShare = 1
StakerAAccumulatedRewards = BlockRewards * StakerAShare
StakerAAccumulatedRewards = $10

RewardsPerBlock = $1 On block 0,
Staker A deposits $100 On block 10,
Staker B deposits $400 On block 15,
Staker A harvests all rewards On block 25,
Staker B harvests all rewards On block 30,
both stakers harvests all rewards.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./RewardToken.sol";

contract StakingManager is Ownable{
using SafeERC20 for IERC20; // Wrappers around ERC20 operations that throw on failure

RewardToken public rewardToken; // Token to be payed as reward

uint256 private rewardTokensPerBlock; // Number of reward tokens minted per block
uint256 private constant STAKER_SHARE_PRECISION = 1e12; // A big number to perform mul and div operations

// Staking user for a pool
struct PoolStaker {
uint256 amount; // The tokens quantity the user has staked.
uint256 rewards; // The reward tokens quantity the user can harvest
uint256 lastRewardedBlock; // Last block number the user had their rewards calculated
}

// Staking pool
struct Pool {
IERC20 stakeToken; // Token to be staked
uint256 tokensStaked; // Total tokens staked
address[] stakers; // Stakers in this pool
}

Pool[] public pools; // Staking pools

// Mapping poolId => staker address => PoolStaker
mapping(uint256 => mapping(address => PoolStaker)) public poolStakers;

// Events
event Deposit(address indexed user, uint256 indexed poolId, uint256 amount);
event Withdraw(address indexed user, uint256 indexed poolId, uint256 amount);
event HarvestRewards(address indexed user, uint256 indexed poolId, uint256 amount);
event PoolCreated(uint256 poolId);

// Constructor
constructor(address _rewardTokenAddress, uint256 _rewardTokensPerBlock) {
rewardToken = RewardToken(_rewardTokenAddress);
rewardTokensPerBlock = _rewardTokensPerBlock;
}

/**
* @dev Create a new staking pool
*/
function createPool(IERC20 _stakeToken) external onlyOwner {
Pool memory pool;
pool.stakeToken = _stakeToken;
pools.push(pool);
uint256 poolId = pools.length - 1;
emit PoolCreated(poolId);
}

/**
* @dev Add staker address to the pool stakers if it's not there already
* We don't have to remove it because if it has amount 0 it won't affect rewards.
* (but it might save gas in the long run)
*/
function addStakerToPoolIfInexistent(uint256 _poolId, address depositingStaker) private {
Pool storage pool = pools[_poolId];
for (uint256 i; i < pool.stakers.length; i++) {
address existingStaker = pool.stakers[i];
if (existingStaker == depositingStaker) return;
}
pool.stakers.push(msg.sender);
}

/**
* @dev Deposit tokens to an existing pool
*/
function deposit(uint256 _poolId, uint256 _amount) external {
require(_amount > 0, "Deposit amount can't be zero");
Pool storage pool = pools[_poolId];
PoolStaker storage staker = poolStakers[_poolId][msg.sender];

// Update pool stakers
updateStakersRewards(_poolId);
addStakerToPoolIfInexistent(_poolId, msg.sender);

// Update current staker
staker.amount = staker.amount + _amount;
staker.lastRewardedBlock = block.number;

// Update pool
pool.tokensStaked = pool.tokensStaked + _amount;

// Deposit tokens
emit Deposit(msg.sender, _poolId, _amount);
pool.stakeToken.safeTransferFrom(
address(msg.sender),
address(this),
_amount
);
}

/**
* @dev Withdraw all tokens from an existing pool
*/
function withdraw(uint256 _poolId) external {
Pool storage pool = pools[_poolId];
PoolStaker storage staker = poolStakers[_poolId][msg.sender];
uint256 amount = staker.amount;
require(amount > 0, "Withdraw amount can't be zero");

// Update pool stakers
updateStakersRewards(_poolId);

// Pay rewards
harvestRewards(_poolId);

// Update staker
staker.amount = 0;

// Update pool
pool.tokensStaked = pool.tokensStaked - amount;

// Withdraw tokens
emit Withdraw(msg.sender, _poolId, amount);
pool.stakeToken.safeTransfer(
address(msg.sender),
amount
);
}
/**
* @dev Harvest user rewards from a given pool id
*/
function harvestRewards(uint256 _poolId) public {
updateStakersRewards(_poolId);
PoolStaker storage staker = poolStakers[_poolId][msg.sender];
uint256 rewardsToHarvest = staker.rewards;
staker.rewards = 0;
emit HarvestRewards(msg.sender, _poolId, rewardsToHarvest);
rewardToken.mint(msg.sender, rewardsToHarvest);
}
/**
* @dev Loops over all stakers from a pool, updating their accumulated rewards according
* to their participation in the pool.
*/
function updateStakersRewards(uint256 _poolId) private {
Pool storage pool = pools[_poolId];
for (uint256 i; i < pool.stakers.length; i++) {
address stakerAddress = pool.stakers[i];
PoolStaker storage staker = poolStakers[_poolId][stakerAddress];
if (staker.amount == 0) return;
uint256 stakedAmount = staker.amount;
uint256 stakerShare = (stakedAmount * STAKER_SHARE_PRECISION / pool.tokensStaked);
uint256 blocksSinceLastReward = block.number - staker.lastRewardedBlock;
uint256 rewards = (blocksSinceLastReward * rewardTokensPerBlock * stakerShare) / STAKER_SHARE_PRECISION;
staker.lastRewardedBlock = block.number;
staker.rewards = staker.rewards + rewards;
}
}
}

Превью BlockFirst

If we use the following formula, which represents the staker's share as the ratio of their tokens to the total tokens in the pool:

In that case, the staker's reward becomes the sum of the products of the rewards allocated in each block range and their share of participation within that range—up to the current moment.

And if we consider a staker's reward from block N to M as the product of the total rewards distributed during that range and their share of participation within the same interval.

The function updateStakersRewards is responsible for iterating over all stakers and updating their accumulated rewards each time someone makes a deposit, withdrawal, or claims rewards.
But what if we could avoid this cycle altogether?

That's a full 20% gas savings, even with just two stakers. That's why the SushiSwap MasterChef contract resembles the last approach I showed. In fact, it's even more efficient because it doesn't have a separate harvestRewards function — reward collection occurs during the deposit function when called with an amount of 0.

For the latter approach (using AccumulatedRewardsPerShare):

Using hardhat-gas-reporter, we can see how much gas each implementation consumes. For the first approach (with the loop over all stakers):

I wasn't able to continue my project without understanding how most DeFi protocols calculate their rewards. I spent the last few days studying how the SushiSwap contract works. I was able to grasp the meaning of some variables in MasterChef (especially accSushiPerShare and rewardDebt) only after implementing and working through the reward system's math myself. Although I found materials explaining the contract, they were all too superficial. Therefore, I decided to explain everything in my own words. I hope this will be helpful to others who are also delving deeper into DeFi.

Conclusion

Since accSushiPerShare can be a number with decimal places, and Solidity does not support floating-point numbers, they multiply sushiReward by a large number, such as 1e12, during calculations, and then divide by the same number when using the value.

What about multiplying and dividing by 1e12?

The main reason we avoid the loop is to save gas. As you can imagine, the more stakers there are, the more expensive the updateStakersRewards function becomes. We can compare the gas costs of both approaches using a test in Hardhat:

Gas savings

Using the previous contract as a basis, we can simply calculate accumulatedRewardsPerShare within the updatePoolRewards function (renamed from updateStakersRewards) and update each staker's rewardDebt whenever they perform an action.
You can see the changes in the code in this commit.

Implementation of AccumulatedRewardsPerShare

We know that the rewards received by Staker A are the sum of their first and last reward claims, i.e., for blocks 0 to 15 and from 15 to 30. We also know that we can obtain the same value using the formula for StakerARewards that we just used above.

Determining rewardDebt

With this approach, each time a deposit or reward withdrawal occurs, we need to iterate over all stakers and recalculate their accumulated rewards. Here is a simple staking contract implementing this method:

Let's apply some mathematics

With this approach, each time a deposit or reward withdrawal occurs, we need to iterate over all stakers and recalculate their accumulated rewards. Here is a simple staking contract implementing this method:

Implementation

Стейкер B снимает $12, и его накопленные награды (StakerBAccumulatedRewards) сбрасываются до 0. Наконец, оба стейкера забирают свои вознаграждения на блоке 30.

Staker A withdraws 11,andtheiraccumulatedrewards(StakerAAccumulatedRewards)areresetto0.StakerBhasaccumulated4 over the last 5 blocks. Then, another 10 blocks pass, and Staker B also decides to claim their rewards.

I started by implementing staking, using a smart contract from a DeFi service I use as an example. It turned out that most staking contracts are copies of SushiSwap's MasterChef. By reading the contract, I understood how staking rewards are actually calculated.

That is, it is clear that tokens are created for each block and distributed among all stakers proportionally to their contribution to the pool.
However, it is unclear what role the variables accSushiPerShare and rewardDebt play in this calculation.
In this post, I want to share how I managed to understand the logic of the MasterChef contract and explain why it was written in this way.
First, let's try to figure out what would be a fair reward for stakers.

At block 10, Staker B deposits $400. Now, at block 15, Staker A withdraws their rewards. Although from blocks 0 to 10 they received 100% of the rewards, from blocks 10 to 15 they are entitled to only 20% (1/5) of the rewards due to their share of participation.

Staker A deposited 100atblock0,andtenblockslater,StakerBdeposited400. During the first ten blocks, Staker A received 100% of the rewards, which amounts to $10.

Suppose that...

Simple reward simulation

As part of my goal to transition from Web2 to Web3 development, I am creating a DeFi application from scratch to learn and practice Solidity.

Introduction

uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);
uint256 sushiReward = multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div( totalAllocPoint );
accSushiPerShare = accSushiPerShare.add( sushiReward.mul(1e12).div(lpSupply) );
}
return user.amount.mul(accSushiPerShare).div(1e12).sub(user.rewardDebt);

PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][_user];
uint256 accSushiPerShare = pool.accSushiPerShare;
uint256 lpSupply = pool.lpToken.balanceOf(address(this));
if (block.number > pool.lastRewardBlock && lpSupply != 0) {

external
view
returns (uint256)

{

function pendingSushi(uint256 _pid, address _user)

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./RewardToken.sol";

contract StakingManager is Ownable{
using SafeERC20 for IERC20; // Wrappers around ERC20 operations that throw on failure

RewardToken public rewardToken; // Token to be payed as reward

uint256 private rewardTokensPerBlock; // Number of reward tokens minted per block
uint256 private constant STAKER_SHARE_PRECISION = 1e12; // A big number to perform mul and div operations

// Staking user for a pool
struct PoolStaker {
uint256 amount; // The tokens quantity the user has staked.
uint256 rewards; // The reward tokens quantity the user can harvest
uint256 lastRewardedBlock; // Last block number the user had their rewards calculated
}

// Staking pool
struct Pool {
IERC20 stakeToken; // Token to be staked
uint256 tokensStaked; // Total tokens staked
address[] stakers; // Stakers in this pool
}

Pool[] public pools; // Staking pools

// Mapping poolId => staker address => PoolStaker
mapping(uint256 => mapping(address => PoolStaker)) public poolStakers;

// Events
event Deposit(address indexed user, uint256 indexed poolId, uint256 amount);
event Withdraw(address indexed user, uint256 indexed poolId, uint256 amount);
event HarvestRewards(address indexed user, uint256 indexed poolId, uint256 amount);
event PoolCreated(uint256 poolId);

// Constructor
constructor(address _rewardTokenAddress, uint256 _rewardTokensPerBlock) {
rewardToken = RewardToken(_rewardTokenAddress);
rewardTokensPerBlock = _rewardTokensPerBlock;
}

/**
* @dev Create a new staking pool
*/
function createPool(IERC20 _stakeToken) external onlyOwner {
Pool memory pool;
pool.stakeToken = _stakeToken;
pools.push(pool);
uint256 poolId = pools.length - 1;
emit PoolCreated(poolId);
}

/**
* @dev Add staker address to the pool stakers if it's not there already
* We don't have to remove it because if it has amount 0 it won't affect rewards.
* (but it might save gas in the long run)
*/
function addStakerToPoolIfInexistent(uint256 _poolId, address depositingStaker) private {
Pool storage pool = pools[_poolId];
for (uint256 i; i < pool.stakers.length; i++) {
address existingStaker = pool.stakers[i];
if (existingStaker == depositingStaker) return;
}
pool.stakers.push(msg.sender);
}

/**
* @dev Deposit tokens to an existing pool
*/
function deposit(uint256 _poolId, uint256 _amount) external {
require(_amount > 0, "Deposit amount can't be zero");
Pool storage pool = pools[_poolId];
PoolStaker storage staker = poolStakers[_poolId][msg.sender];

// Update pool stakers
updateStakersRewards(_poolId);
addStakerToPoolIfInexistent(_poolId, msg.sender);

// Update current staker
staker.amount = staker.amount + _amount;
staker.lastRewardedBlock = block.number;

// Update pool
pool.tokensStaked = pool.tokensStaked + _amount;

// Deposit tokens
emit Deposit(msg.sender, _poolId, _amount);
pool.stakeToken.safeTransferFrom(
address(msg.sender),
address(this),
_amount
);
}

/**
* @dev Withdraw all tokens from an existing pool
*/
function withdraw(uint256 _poolId) external {
Pool storage pool = pools[_poolId];
PoolStaker storage staker = poolStakers[_poolId][msg.sender];
uint256 amount = staker.amount;
require(amount > 0, "Withdraw amount can't be zero");

// Update pool stakers
updateStakersRewards(_poolId);

// Pay rewards
harvestRewards(_poolId);

// Update staker
staker.amount = 0;

// Update pool
pool.tokensStaked = pool.tokensStaked - amount;

// Withdraw tokens
emit Withdraw(msg.sender, _poolId, amount);
pool.stakeToken.safeTransfer(
address(msg.sender),
amount
);
}

/**
* @dev Harvest user rewards from a given pool id
*/
function harvestRewards(uint256 _poolId) public {
updateStakersRewards(_poolId);
PoolStaker storage staker = poolStakers[_poolId][msg.sender];
uint256 rewardsToHarvest = staker.rewards;
staker.rewards = 0;
emit HarvestRewards(msg.sender, _poolId, rewardsToHarvest);
rewardToken.mint(msg.sender, rewardsToHarvest);
}

/**
* @dev Loops over all stakers from a pool, updating their accumulated rewards according
* to their participation in the pool.
*/
function updateStakersRewards(uint256 _poolId) private {
Pool storage pool = pools[_poolId];
for (uint256 i; i < pool.stakers.length; i++) {
address stakerAddress = pool.stakers[i];
PoolStaker storage staker = poolStakers[_poolId][stakerAddress];
if (staker.amount == 0) return;
uint256 stakedAmount = staker.amount;
uint256 stakerShare = (stakedAmount * STAKER_SHARE_PRECISION / pool.tokensStaked);
uint256 blocksSinceLastReward = block.number - staker.lastRewardedBlock;
uint256 rewards = (blocksSinceLastReward * rewardTokensPerBlock * stakerShare) / STAKER_SHARE_PRECISION;
staker.lastRewardedBlock = block.number;
staker.rewards = staker.rewards + rewards;
}
}
}

StakerARewards =
(BlockRewardsOn0to10 * StakerAShareOn0to10) +
(BlockRewardsOn10to15 * StakerAShareOn10to15) +
(BlockRewardsOn15to25 * StakerAShareOn15to25) +
(BlockRewardsOn25to30 * StakerAShareOn25to30)

StakerARewards = 14

it.only("Harvest rewards according with the staker pool's share", async function ()
{ // Arrange Pool
const stakeToken = rewardToken;
await stakeToken.transfer( account2.address, ethers.utils.parseEther("200000") // 200.000 );
await createStakingPool(stakingManager, stakeToken.address);
const amount1 = ethers.utils.parseEther("80");
const amount2 = ethers.utils.parseEther("20");

// Arrange Account1 staking await stakeToken.approve(stakingManager.address, amount1);
await stakingManager.deposit(0, amount1);

// Arrange Account 2 staking await stakeToken.connect(account2).approve(stakingManager.address, amount2);
await stakingManager.connect(account2).deposit(0, amount2);

// Act const acc1HarvestTransaction = await stakingManager.harvestRewards(0);
const acc2HarvestTransaction = await stakingManager .connect(account2) .harvestRewards(0);

// Assert // 2 blocks with 100% participation = 4 reward tokens * 2 blocks = 8 // 1 block with 80% participation = 3.2 reward tokens * 1 block = 3.2 // Account1 Total = 8 + 3.2 = 11.2 reward tokens const expectedAccount1Rewards = ethers.utils.parseEther("11.2");

await expect(acc1HarvestTransaction) .to.emit(stakingManager, "HarvestRewards") .withArgs(account1.address, 0, expectedAccount1Rewards); // 2 block with 20% participation = 0.8 reward tokens * 2 block // Account 1 Total = 1.6 reward tokens const expectedAccount2Rewards = ethers.utils.parseEther("1.6");

await expect(acc2HarvestTransaction) .to.emit(stakingManager, "HarvestRewards") .withArgs(account2.address, 0, expectedAccount2Rewards); });

StakerATokens = 100
AccumulatedRewardsPerShare = (10 / 100) + (5 / 500) + (10 / 500) + (5 / 500)
AccumulatedRewardsPerShare = (10 / 100) + (5 / 500)

StakerARewardsOn15to30 = StakerATokens *
(AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)
StakerARewardsOn15to30 = 100 * ((10 / 500) + (5 / 500))
StakerARewardsOn15to30 = 3

So yes, it works. This means that if we store the value of AccumulatedRewardsPerShare multiplied by the staker’s token amount every time they deposit or withdraw, we can use that value to simply subtract it from their total rewards. In the MasterChef contract, this is called rewardDebt. It’s similar to calculating the staker’s total rewards from block 0, but subtracting the rewards they’ve already collected, or the ones they weren’t entitled to because they weren’t staking yet.

We can also plug in some values to check if this really works.

StakerBRewards = StakerBTokens *
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)

Now, you may have noticed that we can once again isolate StakerATokens.

StakerARewardsOn15to30 = StakerATokens *
(AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)

Now, you may have noticed that we can once again isolate StakerATokens.

StakerARewardsOn15to30 = StakerATokens *
AccumulatedRewardsPerShare - StakerATokens *
AccumulatedRewardsPerShareOn0to15

And replace StakerARewardsOn0to15 in the previous formula.

StakerARewardsOn0to15 = StakerATokens *
AccumulatedRewardsPerShareOn0to15

Now, we can use the following formula for blocks 0 to 15.

StakerARewardsOn15to30 = StakerATokens *
AccumulatedRewardsPerShare - StakerARewardsOn0to15

We get

StakerARewardsOn15to30 = StakerARewards - StakerARewardsOn0to15
StakerARewards = StakerATokens * AccumulatedRewardsPerShare

If we isolate StakerARewardsOn15to30 in the first formula and replace StakerATokens with the value from the second formula, then

StakerARewards = StakerARewardsOn0to15 + StakerARewardsOn15to30
StakerARewards = StakerATokens * AccumulatedRewardsPerShare

StakerBRewards = StakerBTokens *
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)

This is important because although we can use AccumulatedRewardsPerShare to calculate each staker's rewards, we need to subtract the RewardsPerShare that accumulated before their deposit or reward claim action. Let's find out how much Staker A earned during their first reward claim, using what we've learned so far.

Since AccumulatedRewardsPerShare is the same for all stakers, we can say that StakerBRewards equals this value minus the rewards they did not receive for blocks 0 to 10.

StakerARewards = StakerATokens *
AccumulatedRewardsPerShare

Then, we can say that StakerARewards is the product of StakerATokens and AccumulatedRewardsPerShare.

AccumulatedRewardsPerShare =
RewardsPerShareOn0to10 +
RewardsPerShareOn10to15 +
RewardsPerShareOn15to25 +
RewardsPerShareOn25to30

And instead of accSushiPerShare, we will refer to their sum as AccumulatedRewardsPerShare.

RewardsPerShareOn0to10 = (10 / 100)
RewardsPerShareOn10to15 = (5 / 500)
RewardsPerShareOn15to25 = (10 / 500)
RewardsPerShareOn25to30 = (5 / 500)

(5 / 500) + (10 / 500) + (5 / 500)

The SushiSwap contract refers to this sum as accSushiPerShare, so let's call each division RewardsPerShare.

StakerBRewards = 16

StakerBRewards =
400 * ( (5 / 500) +
(10 / 500) +
(5 / 500)
)

StakerBRewards =
StakerBTokens * ( (BlockRewardsOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 / TotalTokensOn25to30)
)

StakerBRewards =
(BlockRewardsOn10to15 * StakerBTokens / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerBTokens / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerBTokens / TotalTokensOn25to30)

Now that the rewards for both stakers match what we observed earlier, let's see what can be reused in calculating each one's rewards. As you can see, both formulas for the stakers' rewards contain a common sum of divisions.

Which aligns with our expectations. Let's do the same for Staker B.

StakerARewards = 100 * (
(10 / 100) +
(5 / 500) +
(10 / 500) +
(5 / 500)
)

We can verify that this works in our scenario by replacing these complex terms with numbers and calculating the total rewards for Staker A.

StakerARewards =
StakerATokens * ( (BlockRewardsOn0to10 / TotalTokensOn0to10) +
(BlockRewardsOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 / TotalTokensOn25to30)
)

And, isolating StakerATokens, we obtain the following.

StakerARewards =
(BlockRewardsOn0to10 * StakerATokens / TotalTokensOn0to10) +
(BlockRewardsOn10to15 * StakerATokens / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerATokens / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerATokens / TotalTokensOn25to30)

Then, we can simplify our formula for StakerARewards.

StakerATokensOn0to10 =
StakerATokensOn10to15 =
StakerATokensOn15to25 =
StakerATokensOn25to30 =
StakerATokens

But in this case, the staker had the same amount of tokens contributed across all ranges.

StakerARewards =
(BlockRewardsOn0to10 * StakerATokensOn0to10 / TotalTokensOn0to10) +
(BlockRewardsOn10to15 * StakerATokensOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerATokensOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerATokensOn25to30 / TotalTokensOn25to30)

We have this

StakerAShareOnNtoM = StakerATokensOnNtoM / TotalTokensOnNtoM

StakerANtoMRewards = BlockRewardsOnNtoM * StakerAShareOnNtoM

StakerARewards =
StakerA0to10Rewards +
StakerA10to15Rewards +
StakerA15to25Rewards +
StakerA25to30Rewards

And if we use the following formula, which represents the staker's share as the ratio of their tokens to the total tokens in the pool:

Then, the staker's reward is obtained as the sum of the products between the rewards for each block range and their share of participation within that range—up to the current moment.

And if we consider their reward from block N to M as the product of the rewards distributed over that range and their share of participation in the same range.

The function updateStakersRewards is responsible for iterating over all stakers and updating their accumulated rewards each time someone makes a deposit, withdrawal, or claims rewards.
But what if we could do without this loop?

Это целых 20% экономии газа, даже при всего двух стеекерах. Вот почему контракт MasterChef от SushiSwap похож на последний вариант, который я показал. На самом деле он ещё более эффективен, потому что в нём нет отдельной функции harvestRewards — сбор наград происходит при вызове функции deposit с количеством 0.

Для последней (использование AccumulatedRewardsPerShare):

С помощью hardhat-gas-reporter мы можем увидеть, насколько каждая реализация затратна по газу. Для первой (с циклом по всем стеекерам):

Я не смог продолжить свой проект, не разобравшись, как большинство DeFi рассчитывают свои награды, и провёл последние дни, изучая работу контракта SushiSwap. Я смог понять смысл некоторых переменных MasterChef (особенно accSushiPerShare и rewardDebt) только после того, как сам реализовал и поработал с математикой системы наград. Хотя я находил материалы, объясняющие контракт, все они были слишком поверхностными. Поэтому я решил объяснить всё своими словами. Надеюсь, это будет полезно тем, кто тоже изучает DeFi более глубоко.

Заключение

Поскольку accSushiPerShare может быть числом с десятичными знаками, а Solidity не поддерживает числа с плавающей запятой, они умножают sushiReward на большое число, например 1e12, при вычислениях, а затем делят на то же число при использовании.

Что насчёт умножения и деления на 1e12?

The reason we avoid the loop is mainly to save gas. As you can imagine, the more stakers there are, the more expensive the updateStakersRewards function becomes. We can compare the gas costs of both approaches using a test in Hardhat:

Gas savings

Using the previous contract as a base, we can simply calculate accumulatedRewardsPerShare in the updatePoolRewards function (renamed from updateStakersRewards) and get the staker’s rewardDebt each time they perform an action.
You can see the code changes in this commit.

Implementation of AccumulatedRewardsPerShare

We know that the rewards received by Staker A are the sum of their first and last reward claims, i.e., for blocks 0 to 15 and from 15 to 30. We also know that we can obtain the same value using the formula for StakerARewards that we just used above.

Determining rewardDebt

With this approach, each time a deposit or reward withdrawal occurs, we need to iterate over all stakers and recalculate their accumulated rewards. Here is a simple staking contract with such an implementation:

Let's apply a bit of mathematics.

it.only("Harvest rewards according with the staker pool's share", async function ()
{ // Arrange Pool
const stakeToken = rewardToken;
await stakeToken.transfer( account2.address, ethers.utils.parseEther("200000") // 200.000 );
await createStakingPool(stakingManager, stakeToken.address);
const amount1 = ethers.utils.parseEther("80");
const amount2 = ethers.utils.parseEther("20");

// Arrange Account1 staking await stakeToken.approve(stakingManager.address, amount1);
await stakingManager.deposit(0, amount1);

// Arrange Account 2 staking await stakeToken.connect(account2).approve(stakingManager.address, amount2);
await stakingManager.connect(account2).deposit(0, amount2);

// Act const acc1HarvestTransaction = await stakingManager.harvestRewards(0);
const acc2HarvestTransaction = await stakingManager .connect(account2) .harvestRewards(0);

// Assert // 2 blocks with 100% participation = 4 reward tokens * 2 blocks = 8 // 1 block with 80% participation = 3.2 reward tokens * 1 block = 3.2 // Account1 Total = 8 + 3.2 = 11.2 reward tokens const expectedAccount1Rewards = ethers.utils.parseEther("11.2");

await expect(acc1HarvestTransaction) .to.emit(stakingManager, "HarvestRewards") .withArgs(account1.address, 0, expectedAccount1Rewards); // 2 block with 20% participation = 0.8 reward tokens * 2 block // Account 1 Total = 1.6 reward tokens const expectedAccount2Rewards = ethers.utils.parseEther("1.6");

await expect(acc2HarvestTransaction) .to.emit(stakingManager, "HarvestRewards") .withArgs(account2.address, 0, expectedAccount2Rewards); });

That’s a whole 20% gas savings, even with just two stakers. That’s why SushiSwap’s MasterChef contract resembles the latter approach I showed. In fact, it’s even more efficient because it doesn’t have a separate harvestRewards function — rewards are collected when calling the deposit function with an amount of 0.

For the latter (using AccumulatedRewardsPerShare):

Using hardhat-gas-reporter, we can see how gas-intensive each implementation is. For the first one (with a loop over all stakers):

I couldn’t continue with my project without understanding how most DeFi platforms calculate their rewards, so I spent the last few days studying how the SushiSwap contract works. I was only able to grasp the meaning of some MasterChef variables (especially accSushiPerShare and rewardDebt) after implementing and working through the reward system’s math myself. Although I found materials explaining the contract, they were all too superficial. That’s why I decided to explain everything in my own words. I hope this will be helpful to others who are also diving deeper into DeFi.

Conclusion

Since accSushiPerShare can be a decimal number and Solidity doesn’t support floating-point numbers, they multiply sushiReward by a large number like 1e12 during calculations, then divide by the same number when using it.

What about multiplying and dividing by 1e12?

Share
Иконка поделиться
Link
Иконка копировать
Иконка скопированно
Back to blog
Кнопка назад
Original article
кнопка вперед
Share
Иконка поделиться
Link
Иконка копировать
Иконка скопировано
Back to blog
Кнопка назад
Original article
кнопка вперед
сетка BlockFirst
сетка BlockFirst
сетка BlockFirst

For requests from users

hello@blockfirst.io

Icon mail

For business inquiries

business@blockfirst.io

Icon mail

Telegram for quick replies

Icon mail

company

Community

media

By signing up for the newsletter, you can be sure we won't spam you :)

News. Specials. Announcements

To top
© 2025-2026 BlockFirst. All rights reserved.
Сетка BlockFirst
hello@blockfirst.io
For commercial offers
Company
Telegram bot for quick replies
Кнопка копировать
Скопировано