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;
}
}
}
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)