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 rewardsOn block 25 , Staker B harvests all rewardsOn 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 = StakerA0 to10Rewards + StakerA10 to15Rewards + StakerA15 to25Rewards + StakerA25 to30Rewards
用户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)总质押代币数 (BlockRewardsOn0 to10 * StakerATokensOn0 to10 / TotalTokensOn0 to10) + (BlockRewardsOn10 to15 * StakerATokensOn10 to15 / TotalTokensOn10 to15) + (BlockRewardsOn15 to25 * StakerATokensOn15 to25 / TotalTokensOn15 to25) + (BlockRewardsOn25 to30 * StakerATokensOn25 to30 / TotalTokensOn25 to30)
用户A每组区块的质押代币数都是一样的,本金没取过
1 2 3 4 5 StakerATokensOn0 to10 = StakerATokensOn10 to15 = StakerATokensOn15 to25 = StakerATokensOn25 to30 = StakerATokens
简化我们的StakerARewards
公式:
1 2 3 4 5 6 StakerARewards = StakerATokens * ( (BlockRewardsOn0 to10 / TotalTokensOn0 to10) + (BlockRewardsOn10 to15 / TotalTokensOn10 to15) + (BlockRewardsOn15 to25 / TotalTokensOn15 to25) + (BlockRewardsOn25 to30 / TotalTokensOn25 to30) )
代入实际数字验证一下该公式:
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 )
SushiSwap
称这个除法总和为accSushiPerShare
每一部分,我们定义为RewardsPerShare
1 2 3 4 5 区块区间奖励 / 区块区间总质押数 = 区块区间 每个质押Token 可以奖励多少 RewardsPerShareOn0 to10 = (10 / 100 ) RewardsPerShareOn10 to15 = (5 / 500 )RewardsPerShareOn15 to25 = (10 / 500 )RewardsPerShareOn25 to30 = (5 / 500 )
累加起来定义为AccumulatedRewardsPerShare
1 2 3 4 5 AccumulatedRewardsPerShare = RewardsPerShareOn0 to10 + RewardsPerShareOn10 to15 + RewardsPerShareOn15 to25 + RewardsPerShareOn25 to30
用户A奖励 = 质押代币数 * 累加的 每个token的奖励数量
1 StakerARewards = StakerATokens * AccumulatedRewardsPerShare
由于AccumulatedRewardsPerShare
对所有用户来说都是一样的,对于用户来说,StakerBRewards
获得的奖励的数量需要减去用户B在区块0~10获得的奖励
1 StakerBRewards = StakerBTokens * (AccumulatedRewardsPerShare - RewardsPerShareOn0 to10)
我们可以将AccumulatedRewardsPerShare
用于每个质押者的奖励计算,我们也必须减去RewardsPerShare
他们的存款/取款操作之前产生的奖励。
Key Point~
StakerARewards
使用上面刚刚使用的公式得到相同的值
1 2 StakerARewards = StakerARewardsOn0 to15 + StakerARewardsOn15 to30StakerARewards = StakerATokens * AccumulatedRewardsPerShare
调换一下位置,变化一下
1 2 StakerARewardsOn15 to30 = StakerARewards - StakerARewardsOn0 to15StakerARewards = StakerATokens * AccumulatedRewardsPerShare
可以得到
1 2 3 StakerARewardsOn15 to30 = StakerATokens * AccumulatedRewardsPerShare - StakerARewardsOn0 to15
对块 0 到 15 使用以下公式:
1 2 StakerARewardsOn0 to15 = StakerATokens * AccumulatedRewardsPerShareOn0 to15
并替换上一个公式中的StakerARewardsOn0to15
1 2 3 StakerARewardsOn15 to30 = StakerATokens * AccumulatedRewardsPerShare -StakerATokens * AccumulatedRewardsPerShareOn0 to15
提取公因子StakerATokens
:
1 2 StakerARewardsOn15 to30 = StakerATokens * (AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0 to15)
这与我们之前得到的公式StakerBRewards
非常相似
1 2 StakerBRewards = StakerBTokens * (AccumulatedRewardsPerShare - RewardsPerShareOn0 to10)
代入数据验证一下:
1 2 3 4 5 6 7 StakerATokens = 100 AccumulatedRewardsPerShare = (10 / 100 ) + (5 / 200 ) + (10 / 200 ) + (5 / 200 )AccumulatedRewardsPerShareOn0 to15 = (10 / 100 ) + (5 / 200 )StakerARewardsOn15 to30 = StakerATokens * (AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0 to15)StakerARewardsOn15 to30 = 100 * ((10 / 200 ) + (5 / 200 )) = 100 * (0.05 + 0.025 ) = 7.5 StakerARewardsOn15 to30 = 7.5
验证正确,这意味着,如果我们在每次存款或取款时,保存一下BlockNtoMRewardsPerShare
,累加成AccumulatedRewardsPerShare
.
再减去之前没参加的区块奖励。
这就是MasterChef里的rewardDebt
(奖励欠款)的含义。这就像计算一个质押者自第0块以来的总奖励,但删除他们已经获得的奖励或他们没有资格获得的奖励,因为他们还没有质押。
gas比较:
我们一开始的思路是 Pool里总质押量发生变化时,要遍历所有用户并更新他们的累加奖励。现在的思路是:总质押量变化时,仅更新AccumulatedRewardsPerShare
引用文章 里的.
使用hardhat-gas-reporter
,我们可以看到每个实现的成本是多少。
对于第一个思路(遍历所有质押者):
对于第二个思路(使用 AccumulatedRewardsPerShare):
即使只有两个质押者,这也节省了 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 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 ); }
deposit质押本金函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 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); }
奖励公式注释:
待奖励数量 = (用户质押数量 * 累加的(每区块每个token可以奖励的数量)) - user.rewardDebt
SushiPerShare
: 区块区间奖励 / 区块区间总质押数 = 区块区间 每个质押Token可以奖励多少Sushi = SushiPerShare 区间单位token奖励数量
accSushiPerShare
:SushiPerShare累加之和,注意是从开始奖励区块 就一直累加的
user.amount * pool.accSushiPerShare
: 假设了该用户从0/开始奖励区块就进入了, 至今的总奖励
user.rewardDebt
用户的奖励债务
当有用户 质押/提取 本金,就会改变池子的比例,会立即更新:
accSushiPerShare
区间单位token奖励数量 累加值,lastRewardBlock
上一个奖励区块
用户会接受到 待提取的奖励
用户的质押本金 更新
用户的奖励债务 更新
Summary:
Refs: