Mark Kop

Posted on

Understanding SushiSwap's MasterChef staking rewards

π Introduction

As part of my goal to migrate from Web2 to Web3 development, I'm building a DeFi application from scratch to learn and practice solidity.

I've started with the staking implementation and used as reference a smart contract from a DeFi that I've been using lately.

Turns out that most staking contracts are a copy from SushiSwap's MasterChef contract.

While reading the contract, I could understand how the staking rewards were actually calculated.

``````function pendingSushi(uint256 _pid, address _user)
external
view
returns (uint256)
{
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][_user];
uint256 accSushiPerShare = pool.accSushiPerShare;
if (block.number > pool.lastRewardBlock && lpSupply != 0) {
uint256 multiplier =
getMultiplier(pool.lastRewardBlock, block.number);
uint256 sushiReward =
multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(
totalAllocPoint
);
sushiReward.mul(1e12).div(lpSupply)
);
}
return user.amount.mul(accSushiPerShare).div(1e12).sub(user.rewardDebt);
}
``````

I mean, it's easy to see that tokens are minted for each block and distributed between all stakers according to their participation in the pool.

However it's not clear what role the variables `accSushiPerShare` and `rewardDebt` play in this calculation.

In this blog post I want to share how I managed to understand the logic behind this MasterChef contract and explain why it's written this way.

Let's start by first figuring out ourselves what would be a fair reward for stakers.

π§  Simple Rewards Simulation

Let's assume that

``````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.
``````

Staker A deposits \$100 on block 0 and ten blocks later Staker B deposits \$400.
For the first ten blocks, Staker A had 100% of their rewards, which is \$10.

``````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
``````

On block 10, Staker B deposits \$400.
Now on block 15 Staker A is harvesting its rewards.
While they got 100% rewards from blocks 0 to 10, from 10 to 15 they are only getting 20% (1/5)

``````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
``````

Staker A harvests \$11 and `StakerAAccumulatedRewards` resets to 0.
Staker B has accumulated \$4 for these last 5 blocks.
Then 10 more blocks pass and B decides to harvest as well.

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

Staker B harvests \$12 and `StakerBAccumulatedRewards` resets to 0.
Finally, both staker harvest their rewards on block 30.

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

Staker A harvests \$3 and B harvests \$4.
Staker has harvested in total \$14 and B \$16

π The implementation

This way, for each action (Deposit or Harvest) we had to go through all stakers and calculate their accumulated rewards.

Here's a simple staking contract with this implementation:

The `updateStakersRewards` is responsible to loop over all staker and update their accumulated rewards every time someone deposits, withdraws or harvests their earnings.

But what if we could avoid this loop?

π Applying some math manipulation

If we see Staker A rewards as a sum of their rewards on each group of blocks

``````StakerARewards =
StakerA0to10Rewards +
StakerA10to15Rewards +
StakerA15to25Rewards +
StakerA25to30Rewards
``````

And if we see their rewards from the block N to M as the multiplication between the rewards that were distributed in the same range by their share in the same range

``````StakerANtoMRewards = BlockRewardsOnNtoM * StakerAShareOnNtoM
``````

Then we get the staker rewards as the sum of the multiplication between the rewards and their share for each range up to the end

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

And using the following formula that represents the staker share as their tokens divided by the total tokens in the pool

``````StakerAShareOnNtoM = StakerATokensOnNtoM / TotalTokensOnNtoM
``````

We have this

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

But, in this case, the staker had the same amount of tokens deposited at all ranges

``````StakerATokensOn0to10 =
StakerATokensOn10to15 =
StakerATokensOn15to25 =
StakerATokensOn25to30 =
StakerATokens
``````

Then we can simplify our `StakerARewards` formula

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

And by putting `StakerATokens` on evidence we have this

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

We can make sure that it works with our scenario by replacing these big words with numbers and getting the total rewards for Staker A

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

Which matches with we were expecting

Let's do the same for staker B

``````StakerBRewards =
(BlockRewardsOn10to15 * StakerBTokens / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerBTokens / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerBTokens / TotalTokensOn25to30)
``````
``````StakerBRewards = StakerBTokens * (
(BlockRewardsOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 / TotalTokensOn25to30)
)
``````
``````StakerBRewards = 400 * (
(5  / 500) +
(10 / 500) +
(5  / 500)
)
``````
``````StakerBRewards = 16
``````

Now that both stakers rewards are matching with what we've seen before, let's check what we can reuse in both rewards calculation.

As you can see, both stakers rewards formulas have a common sum of divisions

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

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

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

And instead of `accSushiPerShare` we will call their sum `AccumulatedRewardsPerShare`

``````AccumulatedRewardsPerShare =
RewardsPerShareOn0to10 +
RewardsPerShareOn10to15 +
RewardsPerShareOn15to25 +
RewardsPerShareOn25to30
``````

Then we can say that `StakerARewards` is the multiplcation of `StakerATokens` by `AccumulatedRewardsPerShare`

``````StakerARewards = StakerATokens *
AccumulatedRewardsPerShare
``````

Since `AccumulatedRewardsPerShare` is the same for all stakers, we can say that `StakerBRewards` is that value minus the rewards they didn't get from blocks 0to10

``````StakerBRewards = StakerBTokens *
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)
``````

This is important, because even though we can use `AccumulatedRewardsPerShare` for every staker rewards calculation, we have to subtract the `RewardsPerShare` that happened before their Deposit/Harvest action.

Let's find out how much the Staker A has harvested on their first harvest using what we discovered out so far.

πΈ Finding out rewardDebt

We know that the rewards that Staker A got is the sum of their first and last harvest, that is from blocks 0to15 and 15to30.
Also, we know that we can get the same value with the `StakerARewards` formula we just used above

``````StakerARewards = StakerARewardsOn0to15 + StakerARewardsOn15to30
StakerARewards = StakerATokens * AccumulatedRewardsPerShare
``````

If we isolate `StakerARewardsOn15to30` in the first formula and replace its `StakerATokens` with the second one

``````StakerARewardsOn15to30 = StakerARewards - StakerARewardsOn0to15
StakerARewards = StakerATokens * AccumulatedRewardsPerShare
``````

we get

``````StakerARewardsOn15to30 = StakerATokens *
AccumulatedRewardsPerShare - StakerARewardsOn0to15
``````

Now we can use the following formula for blocks 0to15

``````StakerARewardsOn0to15 = StakerATokens *
AccumulatedRewardsPerShareOn0to15
``````

And replace `StakerARewardsOn0to15` in the previous one

``````StakerARewardsOn15to30 =
StakerATokens * AccumulatedRewardsPerShare -
StakerATokens * AccumulatedRewardsPerShareOn0to15
``````

Now you might have noticed that we can isolate `StakerATokens` again

``````StakerARewardsOn15to30 = StakerATokens *
(AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)
``````

And that's very similar to the formula we got for `StakerBRewards` previously

``````StakerBRewards = StakerBTokens *
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)
``````

We can also replace some values to check if it actually works

``````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 yeah, it works.

This means that if we save the `AccumulatedRewardsPerShare` value multiplied by the staker tokens amount each time their deposits or withdraws we can use this value to simply subtract it from their total rewards.

This is called `rewardDebt` on the MasterChef's contract.

It's like calculating a staker total rewards since block 0, but removing the rewards they already harvested or the rewards their were not eligibly to claim because they weren't staking yet.

π The AccumulatedRewardsPerShare implementation

Using the previous contract as base, we can simply calculate `accumulatedRewardsPerShare` on `updatePoolRewards` function (renamed from `updateStakersRewards`) and get the staker `rewardsDebt` each time they perform an action.

You can see the diff code on this commit.

β½ Gas Saving

The reason we are avoiding a loop is mainly to save gas. As you can imagine, the more stakers we have, the more expensive the `updateStakersRewards` function gets.

We can compare both gas spending with a hardhat test:

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

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

// Arrange Account 2 staking
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")

// 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")
});
``````

With hardhat-gas-reporter we can see how much expensive each implementation is.

For the first one (loop over all stakers):

For the last one (use AccumulatedRewardsPerShare):

That's a whole 20% gas saving, even with only two stakers.

That's why SushiSwap's MasterChef contract is similar to the last one I showed.
In fact, is even more efficient because it doesn't have a `harvestRewards` function. The harvesting happens when the `deposit` function is called with amount 0.

β What about the 1e12 mul and div?

Since `accSushiPerShare` can be a number with decimals and Solidity doesn't handle float numbers, they multiply `sushiReward` by a big number like `1e12` when calculating it and then divide it by the same number when using it.

π Conclusion

I couldn't move on with my project without understanding how most DeFis were calculting their rewards and spent my latest days figuring out how the SushiSwap's contract worked.

I could only understand the meaning of some MasterChef variables (specially accSushiPerShare and rewardDept) after implementing and manipulating the math in the rewards system myself.

While I've found some material explaining the contract, all of them were too superficial. So I decided to explain it myself.

I hope this can be helpful for anyone who is also studying DeFi in more depth.

amoweolubusayo

I'm so grateful for this article. It helped me understand the MasterChef contract. Thank you!

DanielCawley

this is very helpful, thank you so much!