Sushiswap MasterChef 和 Synthetix 的质押算法

文章详细介绍了MasterChef和Synthetix的质押算法,通过时间加权分配奖励池,并优化了Gas费用。通过伪代码和图表展示了如何计算和管理奖励分配,并比较了两者的差异。

The MasterChef 和 Synthetix 的staking算法根据用户在池中的时间加权贡献,在stakers之间分配固定的奖励池。为了节省gas,这些算法使用了累积的代币级奖励计数器,并推迟奖励的分配。

假设我们有一个固定的奖励池,包含100,000个REWARD代币,我们希望将其公平地分配给从区块1到区块100的stakers。

我们的目标是每个区块分配1,000个REWARD,按其股份在stakers之间进行划分。

例如,在特定区块时,合约中的staking余额如下:

质押金额 % 的池
Alice 100 25%
Bob 100 25%
Chad 200 50%

那么在该区块分配的1,000个REWARD将如下所示:

质押金额 % 的池 分配的奖励
Alice 100 25% 250
Bob 100 25% 250
Chad 200 50% 500

代币和奖励的术语

质押的代币和staking的奖励可能是相同的货币,也可能不是。为明确起见,我们将其称为代币和奖励——有时称为TOKEN和REWARD。

每个区块发送交易来分配奖励是不切实际的

简单的解决方案是让一个离线机器人在每个区块发送交易,以读取合约中每个staker的TOKEN余额,并根据他们在池中的百分比为他们铸造REWARD。

然而,没有可靠的方法可以确保交易被包含在每个区块中。如果机器人漏掉一个区块,那么用户将会得到比他们预期的更少的奖励。

这个策略还会产生很多交易费用。

每个区块发送交易是不必要的,我们可以为未分配奖励的区块发放“追赶”奖励

假设我们保持一个变量 lastUpdateBlockNumber,它跟踪最后一次发放奖励的时间。我们将计算自上一次奖励以来经过的区块数为 block.number - lastUpdateBlockNumber

我们可以跳过一些区块,在真实分配奖励时进行“追赶”。

下图说明了这一点。

catch up rewards for staking

然而,我们仍然没有好的方法来根据staking比例将我们刚刚铸造的奖励分配给所有stakers。

此外,我们不知道自上次奖励分配以来质押的代币余额是否在之前的时间间隔内保持不变。例如,Chad如果知道我们将在区块100测量余额并在区块99进行了大额存款,以获得更多的奖励份额,情况会怎么样?

这个问题实际上很容易解决。

关键不变性:没有交易,没有余额变化

与每20个区块发送交易的机器人相比,我们可以等待用户通过状态变化函数(例如 deposit()withdraw())与合约进行交互。

在这些函数的调用之间,我们可以确定没有人的余额发生变化。

例如,如果在区块10到区块15之间,Alice持有50%的股份,Bob持有50%的股份,则我们可以发放5,000个REWARD(5个区块乘以1,000),并将每人的股份按50%发放。

Chad或Bob无法“插队”增加他们的余额,因为当他们调用 deposit() 时,会触发奖励分配。而奖励分配函数被编程为不包括他们最近的存款。

请参见下面的图表,显示了时间上余额的变化。只有通过与智能合约交易,才能造成这些变化。

hypothetical staking simulation

然而,这个解决方案并不具备可扩展性。

遍历所有stakers的gas消耗较高

在每当有人调用 deposit()withdraw() 时,要将每个staker的REWARD分配是非常昂贵的,如果有几十个staker。转账和ERC 20代币并不便宜,在循环中多次执行是不可行的。

为了高效地进行staking,只有那些发起状态变化交易的人才能获得转移给他们的奖励。对于那些不领取奖励的人,他们的奖励将被递延。那些奖励将待在合约中,等待他们领取。

这样我们就不必进行一堆ERC 20转账了。

为了有一个高效的解决方案:

  • 我们只能更新与发起交易的账户相关的账户变量。
  • 我们只能更新跟踪所有其他人增加的奖励分配的单个全局变量,不能显式更新每个账户。

与其跟踪账户中的奖励积累,我们跟踪单个质押代币的收益

假设我们可以准确跟踪单个质押代币自“自古以来”(合约开始分配奖励时)以来累积的奖励。

如果可以,那跟踪一个账户累积了多少奖励,只需将他们的代币余额乘以自“自古以来”以来单个代币累积了多少奖励。

假设我们知道一枚自“自古以来”质押的代币到当前时刻已累计12个奖励。如果Alice的质押为100,则她应得1,200个奖励。

这有点像说“在我们银行存下的一美元,自我们开设银行以来获得了0.40美元的利息。如果你在我们开设银行时开立了账户,并且此后没有存款或取款,那么你获得了40%的利息。”

这引出了两个问题:

  1. 我们如何跟踪自“自古以来”单个代币的奖励积累。
  2. 如果Alice自“自古以来”并没有质押,而是最近才存入该怎么办?

我们如何跟踪自“自古以来”单个代币的奖励积累

由于每个区块发放固定数量的奖励(在我们的实例中是1,000),stakers越多,他们获得的固定1,000奖励的份额就越少。无论有多少人质押,仅影响的只是合约中质押代币的总供应量。

考虑以下假设例子。

每个区块发放的奖励 质押代币的供应量 每个区块每个代币的奖励
区块1-5 1,000 100 10
区块6-13 1,000 200 5
区块14-15 1,000 100 10
区块16-20 1,000 500 2

质押代币越多,每个区块的奖励 per token 就越少。较大的质押稀释了奖励,因此单个代币获得的奖励也相应变少。

下图直观地描绘了这一点。红色图表是质押的代币供应量。紫色线表示在那个区块中单个代币累积的奖励数量。区块在x轴上向右移动。这两个变量之间的反关系是显而易见的。

inverse relationship between rewards per token and amount of tokens staked

这里是关键

每次我们进行一次状态变化交易时,我们回顾经过了多少个区块,将该数字乘以每个区块的奖励,然后除以总的质押量。这就是一个代币在该区间内所累积的奖励。我们再将该值添加到一个全局累加器,初始值在“自古以来”时为零。如果我们每当交易发生时都重复这个过程,就能知道自“自古以来”以来单个代币所积累的奖励。

以下是添加了累加器的同一图表。

reward accumulator overlaid on reward per token staked

以下是显示相同值的表格:

每个区块发放的奖励 质押代币的供应量 每个区块每个代币的奖励 区间内的区块数 神奖发放区间内的奖励 每个代币的累积奖励
区块0 0 0 0 0 0 0
区块1-5 1,000 100 10 5 50 50
区块6-13 1,000 200 5 8 40 90
区块14-15 1,000 100 10 2 20 110
区块16-20 1,000 500 2 5 10 120

也就是说,单个代币在我们图示的过程中已累计了120个奖励。

测试案例:奖励自始至今一直质押的Alice

让我们考虑一个非常简单的示例。我们再次在每个区块发放1,000个奖励。在20个区块内,将发放20,000个奖励。

以下是Alice和Bob的活动:

  • Alice从区块1到区块20质押了100个代币。
  • 在区块10,Bob质押了100个代币。
  • 在区块20,Alice应得所有已发放奖励的75%,即15,000个奖励。

从区块1到区块10,每个代币的每个区块的奖励是10(1,000 ÷ 100)。在那10个区块的时间间隔内,每个代币累计获得100个奖励(每个区块每个代币10个奖励 × 10个区块)。

但在区块10时,Bob的质押导致每个代币每个区块的奖励降到5(1,000 ÷ 200)。在接下来的10个区块(区块11至20)时间间隔内,每个代币累计了50个奖励。

因此,从区块1到区块10,单个代币累计的总价值是100,而从11到20的收益为50。因此,单个代币累计的总价值为100 + 50 = 150。

既然Alice质押了100个代币,每个代币积累了150个奖励,她将获得15,000个奖励,确实是所发放总奖励的75%。

如果有人还没有从一开始就质押怎么办?

上面例子中的一个明显异常情况(我们在前面部分中预测了这一点)是,若Bob索取奖励,他也将得到15,000个奖励,因为他在区块20的质押为100,与Alice相同。

为了解决此问题,我们仅希望在Bob存入时,累加器开始计数。

直觉的解决方案是在Bob存入的区块中存储区块号,并在后来进行修正。

然而,更简单的方法是在Bob存入之前计算他“将会”在区块10领取的奖励。例如,在区块10时,每个代币的累积奖励为100。因为Bob存入了100个代币,他理论上可以立即索取10,000个奖励。

为了防止这种情况,我们为Bob设置一个变量,称其为“奖励债务”。在他存入的那一刻,我们将奖励债务设定为存入余额乘以每个代币的奖励计数器。这将阻止他立刻索取奖励,因为在这一刻,他应得的奖励将为零(当前奖励减去奖励债务)。

我们为Bob有一个单独的变量称为“奖励债务”或“已发放奖励”,并将其设定为该假设奖励数。在区块10时,单个代币的累积奖励为100,而Bob的存款为100,因此他的奖励债务为10,000。

如果Bob在区块20索取奖励,我们将获得的15,000个奖励减去10,000的奖励债务。Bob在区块20只能领取5,000个奖励。

MasterChef的伪代码

以下是MasterChef算法的简化版本。我们为清晰起见对原合约中的变量名做了一些修改。我们还省略了与放缩代币小数相关的事件和实现细节。

masterchef staking contract pseudocode

Synthetix和MasterChef之间的差异

Synthetix和MasterChef都使用相同的机制,根据质押数量来累计每个代币的奖励。主要区别在于,Synthetix是通过在用户最后与合约交互时存储奖励账户的快照,而不是跟踪奖励债务。当前奖励账户与快照之间的差额用于计算用户的奖励。

该差额被添加到每个用户的奖励映射中,并在用户调用getRewards()之前在那里累积。这种额外的记账使得Synthetix算法效率较低。

其他差异比较小:

  • MasterChef有 deposit()withdraw()
    • Synthetix有 stake()withdraw()getReward()
  • MasterChef以区块作为时间单位。
    • Synthetix使用时间戳。
  • MasterChef按照上述部分所述向自己铸造奖励。
    • Synthetix假设管理员已将奖励转移到合约中,而不进行奖励铸造。
  • MasterChef在可配置的 startBlocklastRewardBlock 之间分配奖励。
    • Synthetix硬编码在管理员启动计时后分配一周的奖励。Synthetix不一定会将合约中的所有奖励余额分配出,但数量由管理员指定。
  • MasterChef在用户调用 deposit()withdraw() 时,以非零金额转移奖励给用户。
    • Synthetix在一个名为奖励的映射中累积用户应得的奖励,但不在用户明确调用 getRewards() 之前转移给用户。
  • MasterChef支持同一合约内多个池,并按池的权重分配奖励。
    • Synthetix只具有一个池。

有兴趣的读者可以参考 SushiSwap MasterChef Staking的代码

Synthetix的伪代码

下图显示了在deposit()withdraw()getRewards()过程中调用的Synthetix的记账子程序。特别是,这是在存款或取款的余额更新或奖励分配之前进行的。

在下图中,lastUpdateTime 是最近一次用户调用这三个函数的时间。在下面的示例中,声称奖励的用户与之前与合约交互的用户不同。prime '标记表示该变量在子程序结束后的值

An image of bookkeeping subroutine of Synthetix

有兴趣的读者可以 自己查阅Synthetix质押代码

更深入地学习RareSkills

请查看我们的区块链训练营,以学习更多高级的技术web3主题。

最初发布于2023年11月21日

  • 原文链接: rareskills.io/post/staki...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/