Solidity 如何实现质押和奖励合约

本文循序渐进实现了 3 个合约:1. 简单质押奖励,重点介绍如何计算奖励的? 2. 代币化质押奖励,提高质押流动性; 3. ERC4626 代币化金库, 将质押存入到金库中。

质押代币是一种 DeFi 工具,它允许用户在合约中质押代币并获得奖励。它是最重要的 DeFi 原语之一,也是成千上万 tokenomic 模型的基础。

在这篇文章中,我将向你展示如何使用简单实现来构建质押奖励合约。然后,我将展示两个更高级的版本:代币化质押奖励 以及 ERC4626 代币化金库

关于质押

代币质押是指在合约中持有资产以支持做市等协议操作的过程。作为交换,资产持有者(即质押者)获得代币奖励,奖励代币可以是他们存入的相同类型,也可以不是。

给区块链协议中提供服务的用户提供奖励的概念是代币经济的基本原理之一,自 ICO 繁荣时期甚至在此之前就已经存在。Compound和 Curve 在利用奖励推动业务方面非常成功,围绕它们的代币经济设计开发了一整套其他区块链应用。

然而,事实证明,独立质押的合约 k06a 的实现 是使用最广泛,有数百种部署和变体。在Unipool发布之后,再看其他的质押合约,很可能就是从它衍生出来的。

简单质押奖励合约

Unipool 质押合约影响巨大,k06a 是世界级的开发者,但出于教育目的,我决定以更清晰的方式再次实现该算法。

Simple Rewards合约允许用户 "押注"一个 stakingToken,并获得一个 rewardsToken,用户必须 "领取(claim)"这个 rewardsToken。用户可以随时"提取 "质押,但奖励也会停止累积。这是一个无权限合约,将在部署时定义的时间间隔内分配奖励。仅此而已。

简单SimpleRewards.sol 中的操作

<p align="center">SimpleRewards.sol 中的函数</p>

计算奖励的数学

这篇Dan Robinson的文章 对质押合约背后的数学进行了精彩的描述,还有原始论文链接。我将跳过大部分数学符号,用更简单的语言解释他们的工作。

奖励只在一个有限的时间段内分配,首先在时间上均匀分配,然后按每个持有者所投入代币的比例分配。

例如,如果我们要分发 100 万个奖励代币,奖励将在 10,000 秒内分发完毕,那么我们每秒正好分发 100 个奖励代币。如果在某一秒钟内只有两个质押者,一个质押 1 个代币和一个质押 3 个代币,那么第一个质押者在这一秒钟内将获得 25 个奖励代币,而另一个质押者将获得 75 个奖励代币。

在区块链上,如果按每秒分配奖励代币会很复杂,也很昂贵。取而代之的是,我们会累积一个计数器,用于计算直到当前时间为止,质押者单个代币可获得的重奖励,并在每次合约中发生交易时更新这个累积器。

每次交易更新累积器的公式是:上次更新后的时间乘以创建时定义的奖励率,再除以更新时的质押总额。

currentRewardsPerToken = accumulatedRewardsPerToken + elapsed * rate  / totalStaked

每代币奖励累加器(rewardsPerToken accumulator)告诉我们,如果在奖励间隔期开始时质押一个代币,质押者将获得多少奖励。这很有用,但我们希望允许认购者在奖励间隔期开始后也可以质押,而且我们希望允许他们不止一次质押。

为此,我们为每个用户存储了他们最后一次交易时的奖励,以及他们最后一次交易时的每代币奖励累积量。根据这些数据,在任何时间点,我们都可以计算出他们的奖励:

currentUserRewards =
  accumulatedUserRewards +
   userStake * (userRecordedRewardsPerToken - currentRewardsPerToken)

每次用户交易都会更新累积奖励,并记录该用户当前的每代币奖励。这一过程允许用户根据自己的意愿多次质押和取消质押。

实现

实现代码如下(SimpleRewards.sol):


// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";

/// @notice Permissionless staking contract for a single rewards program.
/// From the start of the program, to the end of the program, a fixed amount of rewards tokens will be distributed among stakers.
/// The rate at which rewards are distributed is constant over time, but proportional to the amount of tokens staked by each staker.
/// The contract expects to have received enough rewards tokens by the time they are claimable. The rewards tokens can only be recovered by claiming stakers.
/// This is a rewriting of [Unipool.sol](https://github.com/k06a/Unipool/blob/master/contracts/Unipool.sol), modified for clarity and simplified.
/// Careful if using non-standard ERC20 tokens, as they might break things.

contract SimpleRewards {
    using SafeTransferLib for ERC20;
    using Cast for uint256;

    event Staked(address user, uint256 amount);
    event Unstaked(address user, uint256 amount);
    event Claimed(address user, uint256 amount);
    event RewardsPerTokenUpdated(uint256 accumulated);
    event UserRewardsUpdated(address user, uint256 rewards, uint256 checkpoint);

    struct RewardsPerToken {
        uint128 accumulated;                                        // Accumulated rewards per token for the interval, scaled up by 1e18
        uint128 lastUpdated;                                        // Last time the rewards per token accumulator was updated
    }

    struct UserRewards {
        uint128 accumulated;                                        // Accumulated rewards for the user until the checkpoint
        uint128 checkpoint;                                         // RewardsPerToken the last time the user rewards were updated
    }

    ERC20 public immutable stakingToken;                            // Token to be staked
    uint256 public totalStaked;                                     // Total amount staked
    mapping (address => uint256) public userStake;                  // Amount staked per user

    ERC20 public immutable rewardsToken;                            // Token used as rewards
    uint256 public immutable rewardsRate;                           // Wei rewarded per second among all token holders
    uint256 public immutable rewardsStart;                          // Start of the rewards program
    uint256 public immutable rewardsEnd;                            // End of the rewards program       
    RewardsPerToken public rewardsPerToken;                         // Accumulator to track rewards per token
    mapping (address => UserRewards) public accumulatedRewards;     // Rewards accumulated per user

    constructor(ERC20 stakingToken_, ERC20 rewardsToken_, uint256 rewardsStart_, uint256 rewardsEnd_, uint256 totalRewards)
    {
        stakingToken = stakingToken_;
        rewardsToken = rewardsToken_;
        rewardsStart = rewardsStart_;
        rewardsEnd = rewardsEnd_;
        rewardsRate = totalRewards / (rewardsEnd_ - rewardsStart_); // The contract will fail to deploy if end &lt;= start, as it should
        rewardsPerToken.lastUpdated = rewardsStart_.u128();
    }

    /// @notice Update the rewards per token accumulator according to the rate, the time elapsed since the last update, and the current total staked amount.
    function _calculateRewardsPerToken(RewardsPerToken memory rewardsPerTokenIn) internal view returns(RewardsPerToken memory) {
        RewardsPerToken memory rewardsPerTokenOut = RewardsPerToken(rewardsPerTokenIn.accumulated, rewardsPerTokenIn.lastUpdated);
        uint256 totalStaked_ = totalStaked;

        // No changes if the program hasn't started
        if (block.timestamp &lt; rewardsStart) return rewardsPerTokenOut;

        // Stop accumulating at the end of the rewards interval
        uint256 updateTime = block.timestamp &lt; rewardsEnd ? block.timestamp : rewardsEnd;
        uint256 elapsed = updateTime - rewardsPerTokenIn.lastUpdated;

        // No changes if no time has passed
        if (elapsed == 0) return rewardsPerTokenOut;
        rewardsPerTokenOut.lastUpdated = updateTime.u128();

        // If there are no stakers we just change the last update time, the rewards for intervals without stakers are not accumulated
        if (totalStaked == 0) return rewardsPerTokenOut;

        // Calculate and update the new value of the accumulator.
        rewardsPerTokenOut.accumulated = (rewardsPerT...

剩余50%的内容订阅专栏后可查看

点赞 1
收藏 5
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO