Uniswap V2:质押挖矿原理与源码分析

4,313 阅读7分钟

最近在写uniswap相关业务代码,发现自己对于LPtoken质押挖矿(流动性挖矿)这方面一头雾水,看了很多资料,决定写一篇输出,有不对的地方欢迎大家指正~

一、前言

1.1 Uniswap价值

2021.9.17,uniswap向早期参与交易以及LP(提供流动性)的用户空投了总计1.5亿枚UNI治理代币,Uniswap的交易额和交易量瞬间出现了陡坡式增长,一个月创造了330亿美元的交易额,占到总交易额的近乎60%。UNI的价格也在一个月上涨近乎30%。UNI作为uniswap的平台币,是流动性提供者的权益证明,其升值空间也巨大。

1.2 如何获得UNI

  • 从二级市场直接购买可以获取相对的UNI代币。

  • 流动性挖矿。不同于比特币那种通过矿机来进行挖矿,而是通过流动性挖矿来获取奖励分红。

二、什么是质押挖矿?

质押挖矿是将质押的代币存入矿池中,矿池每分钟或每个新区块产生一些奖励代币,这些奖励代币按照流动性提供者质押的代币份额分发给LP。通俗来说,就是质押代币,获得其他代币的奖励。

2.1 质押挖矿分类

  • 质押交易对得到收益

在uniswap中,任何用户都可以根据流动性池中的A,B两种代币的比例提供对应数量的A,B,即可成为流动性提供者,而流动性提供者可以在用户进行交易的时候以交易的手续费为奖励,手续费一般是0.3%。Uniswap对流动性提供者按照份额分配奖励,而份额的大小取决于注入流动性池的代币数量占整个流动性池数量的比重

  • 质押流动性代币得到收益

如果用户手中有流动性代币,可以将其质押在挖矿合约中,可以锁仓质押,也可随时提取,此合约按照设定的规则,按质押金额和质押时长发放奖励给流动性提供者。在Uniswap中,该奖励一般是UNI

以上两种类型的区别为:第一个是用户方提供收益,第二种是平台以自己的平台币作为收益

三、质押挖矿原理(计算用户得到多少收益)

3.1 正常思路

  • 1.计算矿池中每秒产生的奖励代币数量(相当于速率)

rewardRate = 奖励代币总数 / 质押时长

  • 2.计算每单位质押代币所得到的奖励代币

rewardPerToken = rewardRate / 此时矿池中质押代币的总数

  • 3.计算某个用户得到的收益——遍历所有质押的用户,得到每个用户质押代币的份额

rewards = rewardPerToken * 某个用户质押代币的份额

以上算法是否有什么问题?

众所周知,solidity语言注重gas消耗,如果遍历的用户特别多,就会造成gas耗尽,导致失败。

3.2 正确思路

通过上面,我们知道正常思路是无法用solidity语言写出来,那怎么计算用户挖矿奖励呢?我们可以根据最后一次奖励时间和开始质押时间来计算A用户的挖矿奖励

假设a 表示每秒产生的奖励代币数量(是3.1当中的rewardRate)

P表示此时矿池质押代币的总数

a/P表示每单位质押代币所获得的奖励代币

Tn表示第n分钟开始的时候,累计的每质押代币可以分配的奖励之和,即:Tn = a / P1 + a / P2 + a / P3 + … + a / Pn

假设 用户A在第2分钟开始的时候质押了b个质押代币,用户B在第4分钟开始的时候质押了c个质押代币,那么第6分钟开始的时候,用户A可获得的奖励 :

FFD82A3708C57E02B4E684CFC1284BDB.jpg

T6 表示 用户A在第6分钟开始结算的时候,累积的每单位质押代币收到的奖励之和

T2 表示 用户A在第2分钟开始质押的时候,累积的每单位质押代币收到的奖励之和

b 表示结算的时候用户A的总质押代币数量

可以看出,用户的挖矿奖励 = 用户质押的代币总数 *(结算的时候累计的每单位质押代币获得收益之和 - 质押的时候累计的每单位质押代币获得收益之和

四、源码分析

想看质押挖矿所有代码可以去uniswap github上,我介绍的是简化版的质押挖矿,有些函数我没有写。

一共有四个合约,分别如下:

LPToken.sol:表示质押代币

rewardToken.sol: 表示奖励代币

stakeFactory.sol: 表示工厂合约,主要是部署质押合约

staking.sol: 表示质押合约,主要是一些相关函数

LPToken.solrewardToken.sol 就是主要是继承ERC20合约,用于创建质押代币和奖励代币,接下来就不介绍这两个合约,主要介绍stakeFactory.solstaking.sol

4.1 staking.sol

该合约主要是实现质押挖矿过程中所用到的功能。

  • 导入的依赖

import "./rewardTokens.sol";
import "./LPToken.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
  • 定义变量和常量

rewardUNITokens : 创建奖励代币对象,一般是UNI平台币

stakingLPTokens :创建质押代币对象,一般是LPtoken

stakingFinishTime = block.timestamp + stakingTime : 质押挖矿结束时间

stakingTime : 质押时长

_totalSupply: 矿池中质押代币总量

rewardPerTokenStored: 存储每单位质押代币获取的收益

lastUpdateTime : 上一次更新时间

    rewardTokens public rewardUNITokens;
    LPToken public stakingLPTokens;
    uint public stakingFinishTime = block.timestamp + stakingTime;
    uint public stakingTime;
    uint public rewardRate = 100;
    uint private _totalSupply;
    uint public rewardPerTokenStored;
    uint public lastUpdateTime;
  • 映射

rewards: 用户地址=> 用户获得的收益

balances: 用户地址=> 用户质押代币数量

userRewardsPerToken: 用户地址 => 每单位质押代币获取的收益数

    mapping(address => uint) public rewards;
    mapping(address => uint) public balances;
    mapping(address => uint) public userRewardsPerToken;
  • 事件

Stake: 存储质押者地址和质押了多少代币

Withdraw: 存储此时取回收益的用户地址和取出奖励代币数目

Rewards: 存储用户地址和对应的奖励总数

    event Stake(address sender,uint amounts);
    event Withdraw(address recipient,uint amounts);
    event Rewards(address recipient,uint amounts);
  • 构造函数和修饰器

1.构造函数初始化传入的参数分别为 _rewardUNITokens``,_stakingLPTokens _stakingTime,对应的含义是奖励代币地址,质押代币地址,质押时长

2.modifier用于修饰后面的stake()withdraw()getRewards()三个函数(这三个函数改变了矿池的状态),用于更新一些变量包括上一次状态改变的时间,用户操作之前得到每token奖励代币数量,两个映射函数

    constructor(address _rewardUNITokens,address _stakingLPTokens,uint _stakingTime){
        rewardUNITokens = rewardTokens(_rewardUNITokens);
        stakingLPTokens = LPToken(_stakingLPTokens);
        stakingTime = _stakingTime;
    }
    modifier update(address owner) {
        lastUpdateTime = getLastTime();
        rewardPerTokenStored = rewardUNIPerToken();
        if(owner != address(0)){
            userRewardsPerToken[owner] = rewardPerTokenStored;
            rewards[owner] = allRewardsOfUser(owner);
        }
        _;
    }
  • 主要方法

getLastTime()当前区块时间挖矿结束时间两者中返回最小值。

当挖矿未结束时返回的就是当前区块时间,而挖矿结束后则返回挖矿结束时间。挖矿结束后,lastUpdateTime 也会一直等于挖矿结束时间

    function getLastTime() public view returns(uint){
        return Math.min(block.timestamp, stakingFinishTime);
    }

rewardUNIPerToken()一段时间内每份LPtoken获取的奖励代币数量

用累加计算的方式存储到 rewardPerTokenStored 变量中。当挖矿结束后,则不会再产生增量,rewardPerTokenStored 就不会再增加了。

    function rewardUNIPerToken() public view returns(uint rewardUNI){
       if(_totalSupply == 0){
           rewardUNI = rewardPerTokenStored;
       }
       rewardUNI = rewardPerTokenStored.add(getLastTime().sub(lastUpdateTime).mul(rewardRate).mul(1e18).div(_totalSupply));
   }

allRewardsOfUser() 计算用户挖矿获得的奖励总数

奖励总数 = 用户质押代币份额 * (结算的时候累计每单位质押代币奖励总数 - 质押刚开始每单位质押代币奖励总数) + 之前存储的奖励

   function allRewardsOfUser(address account) public view returns(uint){
       return balances[account].mul(rewardUNIPerToken().sub(userRewardsPerToken[account])).div(1e18).add(rewards[account]);
   }

stake() 用户质押代币

1.判断质押代币数量是否大于0

2.更新矿池中总的质押量

3.更新用户的质押数量

4.将用户质押代币转让到质押合约(矿池)中

    function stake(uint stakingAmounts) public update(msg.sender) {
        require(stakingAmounts > 0);
        _totalSupply = _totalSupply.add(stakingAmounts);
        balances[msg.sender] = balances[msg.sender].add(stakingAmounts);
        stakingLPTokens.transferFrom(msg.sender, address(this), stakingAmounts);
        emit Stake(msg.sender, stakingAmounts);
    }

withdraw()取出质押代币

1.判断取出的质押代币数量是否大于0

2.更新矿池中总的质押量

3.更新用户的质押数量

4.将要取出的质押代币转给用户中

    function withdraw(uint stakingAmounts) public update(msg.sender) {
        require(stakingAmounts > 0);
        _totalSupply = _totalSupply.sub(stakingAmounts);
        balances[msg.sender] = balances[msg.sender].sub(stakingAmounts);
        stakingLPTokens.transferFrom(address(this), msg.sender, stakingAmounts);
        emit Withdraw(msg.sender, stakingAmounts);
    }

getRewards()用户领取挖矿奖励

1.计算用户可以得到多少奖励代币

2.更新rewards映射,令其值为0

3.将奖励转发给用户

    function getRewards() public update(msg.sender) {
        uint stakingRewards = rewards[msg.sender];
        require(stakingRewards > 0);
        rewards[msg.sender] = 0;
        rewardUNITokens.transfer(msg.sender, stakingRewards);

        emit Rewards(msg.sender, stakingRewards);
    }

exitStake()解除质押

1.提取用户质押的所有token

2.领取奖励

   function exitStake() public{
       withdraw(balances[msg.sender]);
       getRewards();
   }