0%

StudyRecord-SushiSwap_MasterChef质押收益计算原理

Pre:

学习下如何计算挖矿收益的源码:MasterChef.sol

The MasterChef gives out a constant number of SUSHI per block, for liquidity providers that stake SLP tokens within contract.

  • 奖励从哪来?

主厨每个区块都给出固定的sushi数量,mint铸造出来的,0地址凭空增发出来😆😆😆

  • 奖励怎么分配?

按质押本金的比例分配给流动性提供者,每区块的奖励是固定的,大户质押的本金多,获得的奖励多

简单模拟奖励:

先自己模拟计算一下

1
2
3
4
5
6
RewardsPerBlock = 1$
On block 0, Staker A deposits $100
On block 10, Staker B deposits $100
On block 15, Staker A harvests all rewards
On block 25, Staker B harvests all rewards
On block 30, both stakers harvests all rewards.

前10个区块,用户A独享全部奖励即10$

1
2
3
4
5
6
7
8
9
10
11
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

10个区块后,用户B质押$100
在第15个区块时,用户A提取所有收益

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
From Block 10 to 15:
BlocksPassed: 5
BlockRewards = BlocksPassed * RewardsPerBlock
BlockRewards = $5
StakerATokens: $100
StakerBTokens: $100
TotalTokens: $200

StakerAShare = StakerATokens / TotalTokens
StakerAShare = 1/2
StakerAAccumulatedRewards = (BlockRewards * StakerAShare) + StakerAAccumulatedRewards
StakerAAccumulatedRewards = $2.5 + $10 = $12.5

StakerBShare = StakerBTokens / TotalTokens
StakerBShare = 1/2
StakerBAccumulatedRewards = BlockRewards * StakerBShare
StakerBAccumulatedRewards = $2.5

用户A提取收益后,StakerAAccumulatedRewards重置为 0。用户B在区块10~15累积奖励为 $2.5

在第25区块时,用户B也提取所有收益

1
2
3
4
5
6
7
8
From Block 15 to 25:
BlocksPassed: 10
BlockRewards: $10
StakerATokens: $100
StakerBTokens: $100
TotalTokens: $200
StakerAAccumulatedRewards: $5
StakerBAccumulatedRewards: $2.5 + $5 = $7.5

用户B提取收益后,StakerBAccumulatedRewards重置为 0。最后,两个用户在第30区块后,同时提取奖励

1
2
3
4
5
6
7
8
From Block 25 to 30:
BlocksPassed: 5
BlockRewards: $5
StakerATokens: $100
StakerBTokens: $100
TotalTokens: $200
StakerAAccumulatedRewards: $5 + $2.5 = $7.5
StakerBAccumulatedRewards: $2.5

30个区块共产生奖励 $30
用户A、B总收益分别为:

  • 12.5 + 7.5 = $20

  • 7.5 + 2.5 = $10

这就引申出了个一般的计算思路:每当有交易触发(用户质押/提取本金),都会导致池子里每个用户的比例发生变化,我们要遍历所有用户并更新他们的累加奖励。但这样频繁操作World State Variable/Storage Variable了,要想办法尽量减少操作次数。

这应该是空间复杂度的问题了

应用数学计算->简化:

用户A的最终质押收益 = 每组区块质押奖励之和

1
2
3
4
5
StakerARewards = 
StakerA0to10Rewards +
StakerA10to15Rewards +
StakerA15to25Rewards +
StakerA25to30Rewards

用户A区块(N~M)的质押收益 = 区块(N~M)总奖励 * 区块(N~M)用户A的份额比例

1
StakerANtoMRewards = BlockRewardsOnNtoM * StakerAShareOnNtoM

区块(N~M)用户A的份额比例 = 区块(N~M)A质押代币数 / 区块(N~M)总质押代币数

1
StakerAShareOnNtoM = StakerATokensOnNtoM / TotalTokensOnNtoM

公式汇总一下:

1
2
3
4
5
6
StakerARewards = 
区块(N~M)总奖励 * 区块(N~M)A质押代币数 / 区块(N~M)总质押代币数
(BlockRewardsOn0to10 * StakerATokensOn0to10 / TotalTokensOn0to10) +
(BlockRewardsOn10to15 * StakerATokensOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 * StakerATokensOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 * StakerATokensOn25to30 / TotalTokensOn25to30)

用户A每组区块的质押代币数都是一样的,本金没取过

1
2
3
4
5
StakerATokensOn0to10 = 
StakerATokensOn10to15 =
StakerATokensOn15to25 =
StakerATokensOn25to30 =
StakerATokens

简化我们的StakerARewards公式:

1
2
3
4
5
6
StakerARewards = StakerATokens * (
(BlockRewardsOn0to10 / TotalTokensOn0to10) +
(BlockRewardsOn10to15 / TotalTokensOn10to15) +
(BlockRewardsOn15to25 / TotalTokensOn15to25) +
(BlockRewardsOn25to30 / TotalTokensOn25to30)
)

代入实际数字验证一下该公式:

1
2
3
4
5
6
StakerARewards = 100 * (
(10 / 100) +
(5 / 200) +
(10 / 200) +
(5 / 200)
)
1
StakerARewards = 100 * ( 0.1 + 0.025 + 0.05 + 0.025) = 100 * 0.2 = 20

与模拟的情况是一样的,对用户B也计算一下:

1
2
3
4
5
StakerBRewards = 100 * (
(5 / 200) +
(10 / 200) +
(5 / 200)
)
1
StakerBRewards = 100 * (0.025 + 0.05 + 0.025) = 10

两个用户都验证正确,然后在计算奖励时,他们有共同的除法总和:

1
(5  / 200) + (10 / 200) + (5  / 200)

20220715231308

SushiSwap 称这个除法总和为accSushiPerShare

每一部分,我们定义为RewardsPerShare

1
2
3
4
5
区块区间奖励 / 区块区间总质押数 = 区块区间 每个质押Token可以奖励多少
RewardsPerShareOn0to10 = (10 / 100)
RewardsPerShareOn10to15 = (5 / 500)
RewardsPerShareOn15to25 = (10 / 500)
RewardsPerShareOn25to30 = (5 / 500)

累加起来定义为AccumulatedRewardsPerShare

1
2
3
4
5
AccumulatedRewardsPerShare = 
RewardsPerShareOn0to10 +
RewardsPerShareOn10to15 +
RewardsPerShareOn15to25 +
RewardsPerShareOn25to30

用户A奖励 = 质押代币数 * 累加的 每个token的奖励数量

1
StakerARewards = StakerATokens * AccumulatedRewardsPerShare

由于AccumulatedRewardsPerShare对所有用户来说都是一样的,对于用户来说,StakerBRewards获得的奖励的数量需要减去用户B在区块0~10获得的奖励

1
StakerBRewards = StakerBTokens * (AccumulatedRewardsPerShare - RewardsPerShareOn0to10)

我们可以将AccumulatedRewardsPerShare用于每个质押者的奖励计算,我们也必须减去RewardsPerShare他们的存款/取款操作之前产生的奖励。

Key Point~

StakerARewards 使用上面刚刚使用的公式得到相同的值

1
2
StakerARewards = StakerARewardsOn0to15 + StakerARewardsOn15to30
StakerARewards = StakerATokens * AccumulatedRewardsPerShare

调换一下位置,变化一下

1
2
StakerARewardsOn15to30 = StakerARewards - StakerARewardsOn0to15
StakerARewards = StakerATokens * AccumulatedRewardsPerShare

可以得到

1
2
3
StakerARewardsOn15to30 = StakerATokens * 
AccumulatedRewardsPerShare - StakerARewardsOn0to15
// 记住 AccumulatedRewardsPerShare 是全阶段的

对块 0 到 15 使用以下公式:

1
2
StakerARewardsOn0to15 = StakerATokens * 
AccumulatedRewardsPerShareOn0to15

并替换上一个公式中的StakerARewardsOn0to15

1
2
3
StakerARewardsOn15to30 = 
StakerATokens * AccumulatedRewardsPerShare -
StakerATokens * AccumulatedRewardsPerShareOn0to15

提取公因子StakerATokens:

1
2
StakerARewardsOn15to30 = StakerATokens * 
(AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)

这与我们之前得到的公式StakerBRewards非常相似

1
2
StakerBRewards = StakerBTokens * 
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)

代入数据验证一下:

1
2
3
4
5
6
7
StakerATokens = 100
AccumulatedRewardsPerShare = (10 / 100) + (5 / 200) + (10 / 200) + (5 / 200)
AccumulatedRewardsPerShareOn0to15 = (10 / 100) + (5 / 200)

StakerARewardsOn15to30 = StakerATokens * (AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)
StakerARewardsOn15to30 = 100 * ((10 / 200) + (5 / 200)) = 100 * (0.05 + 0.025) = 7.5
StakerARewardsOn15to30 = 7.5

验证正确,这意味着,如果我们在每次存款或取款时,保存一下BlockNtoMRewardsPerShare,累加成AccumulatedRewardsPerShare.
再减去之前没参加的区块奖励。

这就是MasterChef里的rewardDebt(奖励欠款)的含义。这就像计算一个质押者自第0块以来的总奖励,但删除他们已经获得的奖励或他们没有资格获得的奖励,因为他们还没有质押。

gas比较:

我们一开始的思路是 Pool里总质押量发生变化时,要遍历所有用户并更新他们的累加奖励。现在的思路是:总质押量变化时,仅更新AccumulatedRewardsPerShare

引用文章里的.
使用hardhat-gas-reporter,我们可以看到每个实现的成本是多少。

对于第一个思路(遍历所有质押者):

20220716102735

对于第二个思路(使用 AccumulatedRewardsPerShare):

20220716102822

即使只有两个质押者,这也节省了 20% 的gas。

经过以上的推导过程后,再来看看源码.

pendingSushi计算待奖励函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// View function to see pending SUSHIs on frontend.
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; // 区块区间奖励 / 区块区间总质押数 = 区块区间 每个质押Token可以奖励多少 = SushiPerShare
// accSushiPerShare = SushiPerShare累加之和
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( // allocPoint是质押池的分配比例
totalAllocPoint
);
accSushiPerShare = accSushiPerShare.add( // SushiPerShare累加
sushiReward.mul(1e12).div(lpSupply) // 由于accSushiPerShare可以是带小数的数字,
// 而 Solidity 不处理浮点数,因此它们在计算时乘以sushiReward一个大数字1e12,然后在使用时除以相同的数字。
);
}
return user.amount.mul(accSushiPerShare).div(1e12).sub(user.rewardDebt);
}
  • SushiPerShare: 区块区间奖励 / 区块区间总质押数 = 区块区间 每个质押Token可以奖励多少 = SushiPerShare

  • accSushiPerShare:SushiPerShare累加之和,注意是从开始奖励区块 就一直累加的

  • rewardDebt奖励债务

deposit质押本金函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Deposit LP tokens to MasterChef for SUSHI allocation.
function deposit(uint256 _pid, uint256 _amount) public { // 质押本金
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][msg.sender];
updatePool(_pid); // 更新池子
if (user.amount > 0) { // 该用户之前有质押过
uint256 pending = // 累积奖励
user.amount.mul(pool.accSushiPerShare).div(1e12).sub(
user.rewardDebt
);
safeSushiTransfer(msg.sender, pending); // 发放之前的奖励
}
pool.lpToken.safeTransferFrom(
address(msg.sender),
address(this),
_amount
);
user.amount = user.amount.add(_amount); // 记录本金增加
user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12); // !!! 记录奖励欠款
emit Deposit(msg.sender, _pid, _amount);
}

withdraw提取本金函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Withdraw LP tokens from MasterChef.
function withdraw(uint256 _pid, uint256 _amount) public { // 提取一定数量的本金
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][msg.sender];
require(user.amount >= _amount, "withdraw: not good");
updatePool(_pid); // 更新池子
uint256 pending =
user.amount.mul(pool.accSushiPerShare).div(1e12).sub(
user.rewardDebt
);
safeSushiTransfer(msg.sender, pending);
user.amount = user.amount.sub(_amount);
user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12);
pool.lpToken.safeTransfer(address(msg.sender), _amount);
emit Withdraw(msg.sender, _pid, _amount);
}

奖励公式注释:

1
2
3
4
5
6
7
8
9
10
// We do some fancy math here. Basically, any point in time, the amount of SUSHIs
// entitled to a user but is pending to be distributed is:
//
// pending reward = (user.amount * pool.accSushiPerShare) - user.rewardDebt
//
// Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens:
// 1. The pool's `accSushiPerShare` (and `lastRewardBlock`) gets updated.
// 2. User receives the pending reward sent to his/her address.
// 3. User's `amount` gets updated.
// 4. User's `rewardDebt` gets updated.

待奖励数量 = (用户质押数量 * 累加的(每区块每个token可以奖励的数量)) - user.rewardDebt

  • SushiPerShare: 区块区间奖励 / 区块区间总质押数 = 区块区间 每个质押Token可以奖励多少Sushi = SushiPerShare 区间单位token奖励数量

  • accSushiPerShare:SushiPerShare累加之和,注意是从开始奖励区块 就一直累加的

  • user.amount * pool.accSushiPerShare: 假设了该用户从0/开始奖励区块就进入了, 至今的总奖励

  • user.rewardDebt 用户的奖励债务

当有用户 质押/提取 本金,就会改变池子的比例,会立即更新:

  1. accSushiPerShare区间单位token奖励数量 累加值,lastRewardBlock上一个奖励区块

  2. 用户会接受到 待提取的奖励

  3. 用户的质押本金 更新

  4. 用户的奖励债务 更新

20220716115809

Summary:

  • 自己推导一下数学公式,利于加深理解

  • think step walk slowly

  • 有些池子前期每区块奖励的数量多,质押的本金少,这个时候大户进入,很有优势

    • 因为总奖励多,占比例大,收益就很高
    • apr高,吸引新用户进来,买矿币,矿币价格↑,暂时没抛压
    • 矿币价格可能在上升期,挖提卖就很爽,双倍快乐 😋😋😋
    • Be a Degen???
    • 20220716120707

Refs: