Sishiswapのマスターシェフを理解する


👋 導入


Web 2からWEB 3の開発に移行する私の目標の一部として、私はゼロからDefiアプリケーションを構築し、学習して、粘り強さを実践しています.


私はstakingの実装から始めて、私が最近使用していたdefiからスマートリファレンスを参照として使用します.
大部分のstaking契約がからのコピーであるとわかりますSushiSwap's MasterChef contract .
契約書を読んでいる間、私は実際に計算された方法を理解することができました.
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);
}
つまり、トークンが各ブロックごとに鋳造され、プールに参加することによってすべてのステッカーの間で分配されるのを見るのは簡単です.
しかし、どのような役割がどのような変数であるかは不明ですaccSushiPerShare and rewardDebt この計算で遊ぶ.
このブログ記事では、このマスターシェフ契約の背後にあるロジックをどのように理解しているかを共有したいと思います.
まず最初にステッカーのための公正な報酬となる自分自身を考え出すことから始めましょう.

🧠 単純報酬シミュレーション


仮定しましょう
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.
スティッカーAはブロック0に10ドル、10ブロック後にステッカーBは400ドルを預けます.
最初の10ブロックでは、Staker Aは100 %の報酬を持っていました.
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は400ドルを預けている.
現在、ブロック15のステッカーAは、その報酬を収穫しています.
彼らはブロック0から10まで、10から15に100 %の報酬を得た一方、彼らは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
ステッカーA収穫11ドルと11StakerAAccumulatedRewards 0にリセットします.
ステッカーBはこれらの最後の5ブロックのために$ 4を蓄積しました.
その後、さらに多くのブロックを通過し、同様に収穫を決定するB.
From Block 15 to 25:
BlocksPassed: 10
BlockRewards: $10
StakerATokens: $100
StakerBTokens: $400
TotalTokens: $500
StakerAAccumulatedRewards: $2
StakerBAccumulatedRewards: $8 + $4
ステッカーBは、12ドルと12ドルを収穫しますStakerBAccumulatedRewards 0にリセットします.
最後に、両方のステッカーは、ブロック30で彼らの報酬を収穫します.
From Block 25 to 30:
BlocksPassed: 5
BlockRewards: $5
StakerATokens: $100
StakerBTokens: $400
TotalTokens: $500
StakerAAccumulatedRewards: $1 + $2
StakerBAccumulatedRewards: $4
ステッカーA収穫3ドルとB収穫4ドル.
ステッカーは合計14ドルとB $ 16で収穫しました

📝 実装


このように、それぞれの行動(預金や収穫)のために、我々はすべてのステッカーを通過し、その蓄積報酬を計算する必要がありました.
以下に、この実装について簡単な手順を示します.
< div >
< p >updateStakersRewards すべてのステッカーの上でループして、誰かが預金して、彼らの収益を収穫するか、収穫するたびに、彼らの蓄積された報酬を更新する責任がありますp >
しかし、私たちがこのループを避けることができるならば、どうですかp >

📐 いくつかの数学操作の適用


私たちがstakerを見るならば、ブロックの各グループの彼らの報酬の合計として報酬
<> P >
クラスをハイライト表示する
StakerARewards = 
StakerA0to10Rewards + 
StakerA10to15Rewards + 
StakerA15to25Rewards + 
StakerA25to30Rewards
< div >
そして、私たちがブロックNからMまで彼らの報酬を同じ範囲で分配される報酬の間の掛け算と同じくらいの範囲で彼らの報酬を見るならば
<> P >
クラスをハイライト表示する
StakerANtoMRewards = BlockRewardsOnNtoM * StakerAShareOnNtoM
< div >
それから、私たちは、ステッカー報酬を報酬と彼らの分け前との間の掛け算の合計として、最後の<br>>までの範囲まで得ます
<> P >
クラスをハイライト表示する
StakerARewards = 
(BlockRewardsOn0to10 * StakerAShareOn0to10) + 
(BlockRewardsOn10to15 * StakerAShareOn10to15) + 
(BlockRewardsOn15to25 * StakerAShareOn15to25) + 
(BlockRewardsOn25to30 * StakerAShareOn25to30)
< div >
そして、以下の式を使用して、それらのトークンが、プール< br/>内の全トークンによって分割されたトークンとして表現される
<> P >
クラスをハイライト表示する
StakerAShareOnNtoM = StakerATokensOnNtoM / TotalTokensOnNtoM
< div >
< br/> < br/>
<> P >
クラスをハイライト表示する
StakerARewards = 
(BlockRewardsOn0to10 * StakerATokensOn0to10 / TotalTokensOn0to10) + 
(BlockRewardsOn10to15 * StakerATokensOn10to15 / TotalTokensOn10to15) + 
(BlockRewardsOn15to25 * StakerATokensOn15to25 / TotalTokensOn15to25) + 
(BlockRewardsOn25to30 * StakerATokensOn25to30 / TotalTokensOn25to30)
< div >
しかし、この場合、stakerは同じ範囲のトークンを持っていた
<> P >
クラスをハイライト表示する
StakerATokensOn0to10 = 
StakerATokensOn10to15 = 
StakerATokensOn15to25 = 
StakerATokensOn25to30 = 
StakerATokens
< div >
それから、我々は我々を単純にすることができますStakerARewards フォーミュラ・B
<> P >
クラスをハイライト表示する
StakerARewards = 
(BlockRewardsOn0to10 * StakerATokens / TotalTokensOn0to10) + 
(BlockRewardsOn10to15 * StakerATokens / TotalTokensOn10to15) + 
(BlockRewardsOn15to25 * StakerATokens / TotalTokensOn15to25) + 
(BlockRewardsOn25to30 * StakerATokens / TotalTokensOn25to30)
< div >
< p >とパッティングStakerATokens 証拠に我々はこの< br/>を持っている
<> P >
クラスをハイライト表示する
StakerARewards = StakerATokens * (
  (BlockRewardsOn0to10 / TotalTokensOn0to10) + 
  (BlockRewardsOn10to15 / TotalTokensOn10to15) + 
  (BlockRewardsOn15to25 / TotalTokensOn15to25) + 
  (BlockRewardsOn25to30 / TotalTokensOn25to30)
)
< div >
私たちは、これらの大きな単語を数字で置き換えて、Stacker A < br/>の合計報酬を得ることで、我々のシナリオで動作することを確認することができます
<> P >
クラスをハイライト表示する
StakerARewards = 100 * (
  (10 / 100) + 
  (5  / 500) + 
  (10 / 500) + 
  (5  / 500)
)
< div >
クラスをハイライト表示する
StakerARewards = 14
< div >
< p >は< tt/
staker b < br/>を使いましょう
<> P >
クラスをハイライト表示する
StakerBRewards = 
(BlockRewardsOn10to15 * StakerBTokens / TotalTokensOn10to15) + 
(BlockRewardsOn15to25 * StakerBTokens / TotalTokensOn15to25) + 
(BlockRewardsOn25to30 * StakerBTokens / TotalTokensOn25to30)
< div >
クラスをハイライト表示する
StakerBRewards = StakerBTokens * (
  (BlockRewardsOn10to15 / TotalTokensOn10to15) + 
  (BlockRewardsOn15to25 / TotalTokensOn15to25) + 
  (BlockRewardsOn25to30 / TotalTokensOn25to30)
)
< div >
クラスをハイライト表示する
StakerBRewards = 400 * (
  (5  / 500) + 
  (10 / 500) + 
  (5  / 500)
)
< div >
クラスをハイライト表示する
StakerBRewards = 16
< div >
現在、両方のスターカーの報酬が前に見たものと一致しているので、両方の報酬計算で再利用できるものをチェックしましょうp >
あなたが見ることができるように、両方のスタッカーは公式の報酬の共通総和
<> P >
クラスをハイライト表示する
(5 / 500) + (10 / 500) + (5 / 500)
< div >
Sishiswapの契約コールこの合計accSushiPerShare , では、各課を呼びましょうRewardsPerShare

<> P >
クラスをハイライト表示する
RewardsPerShareOn0to10  = (10 / 100)
RewardsPerShareOn10to15 = (5  / 500)
RewardsPerShareOn15to25 = (10 / 500)
RewardsPerShareOn25to30 = (5  / 500)
< div >
の代わりにaccSushiPerShare 我々は彼らの合計を電話しますAccumulatedRewardsPerShare

<> P >
クラスをハイライト表示する
AccumulatedRewardsPerShare = 
RewardsPerShareOn0to10 + 
RewardsPerShareOn10to15 + 
RewardsPerShareOn15to25 + 
RewardsPerShareOn25to30
< div >
それから、我々はそれを言うことができますStakerARewardsStakerATokens そばAccumulatedRewardsPerShare

<> P >
クラスをハイライト表示する
StakerARewards = StakerATokens * 
AccumulatedRewardsPerShare
< div >
以降AccumulatedRewardsPerShare すべてのステッカーのために同じです、我々は言うことができますStakerBRewards その値は、ブロック0 to 10 < br/>から得られなかった報酬を引いています
<> P >
クラスをハイライト表示する
StakerBRewards = StakerBTokens * 
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)
< div >
< p >これは重要です、なぜなら私たちが利用できるとしてもAccumulatedRewardsPerShare すべてのステッカー報酬計算のためにRewardsPerShare それは彼らの預金/収穫行為の前に起こりましたp >
ここで見つけたものを使って、最初の収穫で収穫したステーキAの量を調べましょうp >

💸 Reward債務を見つける


私たちは、ステッカーAが得た報酬は、最初の収穫と最後の収穫の合計であることを知っています.br/>
また、我々は同じ値を得ることができることを知っているStakerARewards 我々が上記の< br/>を使った公式
<> P >
クラスをハイライト表示する
StakerARewards = StakerARewardsOn0to15 + StakerARewardsOn15to30
StakerARewards = StakerATokens * AccumulatedRewardsPerShare
< div >
分離するならStakerARewardsOn15to30 最初の式で置換StakerATokens 2番目のもの< br/>
<> P >
クラスをハイライト表示する
StakerARewardsOn15to30 = StakerARewards - StakerARewardsOn0to15
StakerARewards = StakerATokens * AccumulatedRewardsPerShare
< div >
< tag > < br/>
<> P >
クラスをハイライト表示する
StakerARewardsOn15to30 = StakerATokens * 
AccumulatedRewardsPerShare - StakerARewardsOn0to15
< div >
現在、私たちはブロック0 to 15 < br/>のために以下の式を使用できます
<> P >
クラスをハイライト表示する
StakerARewardsOn0to15 = StakerATokens * 
AccumulatedRewardsPerShareOn0to15
< div >
< p >と置換StakerARewardsOn0to15 前の< br/>
<> P >
クラスをハイライト表示する
StakerARewardsOn15to30 = 
StakerATokens * AccumulatedRewardsPerShare -
StakerATokens * AccumulatedRewardsPerShareOn0to15
< div >
あなたは、我々が孤立することができると気がつきましたStakerATokens 再び< br/>
<> P >
クラスをハイライト表示する
StakerARewardsOn15to30 = StakerATokens * 
(AccumulatedRewardsPerShare - AccumulatedRewardsPerShareOn0to15)
< div >
そして、それは我々が得た公式にとても似ていますStakerBRewards 以前は< br/>
<> P >
クラスをハイライト表示する
StakerBRewards = StakerBTokens * 
(AccumulatedRewardsPerShare - RewardsPerShareOn0to10)
< div >
<高橋潤子>
<> P >
クラスをハイライト表示する
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
< div >
< em >そうですねp >
これは、我々が保存するならAccumulatedRewardsPerShare 値は、彼らの預金または撤回をするたびに、ステッカートークン量によって乗算されます.p >
これは呼ばれますrewardDebt マスターシェフの契約についてp >
それはブロック0からステッカー合計報酬を計算するようなものですが、すでに収穫された報酬を削除するか、報酬を取り除くことは、彼らがまだstakingしていなかったので、巧みに主張することでありませんでしたp >

📝 累積されたrewardspershareの実装


前の契約をベースとして、単純に計算できますaccumulatedRewardsPerShare on updatePoolRewards 関数名updateStakersRewards ) そして、ステッカーを得るrewardsDebt 毎回動作します.p >
あなたはdiffコードを見ることができますthis commit .


< div class ="LagagCount - gig - Link - tag "
"スクリプトのID "https://gist.github.com/Markkop/24503047d199121651f4b75545ddbc86.js//>
< div >

⛽ ガス節約


私たちがループを避けている理由は主にガスを節約することです.ご存知のように、我々が持っているより多くのステーキ、より高価なupdateStakersRewards 関数が取得するp >
私たちは、両方のガス費をhardhatテストと比較することができます
<> P >
クラスをハイライト表示する
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);
});
< div >
<ップ>hardhat-gas-reporter それぞれの実装がどれだけ高価かを見ることができます.p >
最初の1つ(ループのすべてのステッカー):<br/>


(最後の1つについては< urlurl ="http ://www . linux . or . jp/">を参照ください).


それは、2台のストーカーだけでさえ、全体の20 %のガス節約ですp >
したがって、Susiiswapのマスターシェフ契約は、私が示した最後のものに類似していますbr/>
実際には、より効率的ですharvestRewards 関数.収穫が起こるとdeposit 関数は量0p >

❓ どのような1 E 12のmulとdiv?


以降accSushiPerShare 小数点以下の数値を持つことができます.sushiReward 大きな数字で1e12 それを計算して、それを使用するとき、同じ数でそれを分割するときp >

🏁 結論


< p >私のプロジェクトではほとんどのDefisが報酬を計算していて、Susiiswapの契約がどのように機能しているかを理解している最新の日々を過ごしました.p >
私はいくつかのマスターシェフ変数(特にaccsusipershareとrewarddec)の意味を理解することができました.p >
私は契約書を説明する資料を見つけましたが、それらのすべてはあまりに浅薄でした.それで私はそれを自分で説明することにしましたp >
<高橋潤子>p >