Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 20, 2022 05:33 pm GMT

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;    uint256 lpSupply = pool.lpToken.balanceOf(address(this));    if (block.number > pool.lastRewardBlock && lpSupply != 0) {        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);}

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 = $1On block 0, Staker A deposits $100On block 10, Staker B deposits $400On block 15, Staker A harvests all rewardsOn block 25, Staker B harvests all rewardsOn 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: 10BlockRewards = BlocksPassed * RewardsPerBlock BlockRewards = $10StakerATokens: $100TotalTokens: $100StakerAShare = StakerATokens / TotalTokensStakerAShare = 1StakerAAccumulatedRewards = BlockRewards * StakerAShareStakerAAccumulatedRewards = $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: 5BlockRewards = BlocksPassed * RewardsPerBlock BlockRewards = $5StakerATokens: $100StakerBTokens: $400TotalTokens: $500StakerAShare = StakerATokens / TotalTokensStakerAShare = 1/5StakerAAccumulatedRewards = (BlockRewards * StakerAShare) + StakerAAccumulatedRewardsStakerAAccumulatedRewards = $1 + $10StakerBShare = StakerBTokens / TotalTokensStakerBShare = 4/5StakerBAccumulatedRewards = BlockRewards * StakerBShareStakerBAccumulatedRewards = $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: 10BlockRewards: $10StakerATokens: $100StakerBTokens: $400TotalTokens: $500StakerAAccumulatedRewards: $2StakerBAccumulatedRewards: $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: 5BlockRewards: $5StakerATokens: $100StakerBTokens: $400TotalTokens: $500StakerAAccumulatedRewards: $1 + $2StakerBAccumulatedRewards: $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 + StakerARewardsOn15to30StakerARewards = StakerATokens * AccumulatedRewardsPerShare

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

StakerARewardsOn15to30 = StakerARewards - StakerARewardsOn0to15StakerARewards = 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 = 100AccumulatedRewardsPerShare = (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(    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);});

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

For the first one (loop over all stakers):
Simple Staking Rewards implementation gas spent

For the last one (use AccumulatedRewardsPerShare):
Efficient Staking Rewards implementation gas spent

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.


Original Link: https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To