美文网首页
抵押挖矿合约 Uniswap StakingReward

抵押挖矿合约 Uniswap StakingReward

作者: walker_1992 | 来源:发表于2021-12-20 18:49 被阅读0次

    源码

    1. 前端 uniswap-interface v3.0.0
    2. uniswap-sdk 3.0.3
    3. liquidity-staker 合约源码

    关于合约

    合约地址: https://github.com/Uniswap/liquidity-stake
    质押挖矿项目合约比较小,只有4个sol文件:

    ├── RewardsDistributionRecipient.sol
    ├── StakingRewards.sol
    ├── StakingRewardsFactory.sol
    ├── interfaces
    │   └── IStakingRewards.sol
    └── test
        └── TestERC20.sol
    

    IStakingRewards.sol

    IStakingRewards.sol是一个接口文件,定义了质押合约的StakingRewards需要实现的一些函数,其中mutative只有四个:

        // Views
        function lastTimeRewardApplicable() external view returns (uint256);      // 有奖励的最近时间
        function rewardPerToken() external view returns (uint256);                      // 每单位Token的奖励数量
        function earned(address account) external view returns (uint256);          // 用户已赚但未提取的奖励数量
        function getRewardForDuration() external view returns (uint256);           // 挖矿奖励总量
        function totalSupply() external view returns (uint256);                             // 总质押量
        function balanceOf(address account) external view returns (uint256);    // 用户的质押
    
        // Mutative
        function stake(uint256 amount) external;        // 充值
        function withdraw(uint256 amount) external;  // 提现,即解质押
        function getReward() external;                       // 提取奖励
        function exit() external;                                   // 退出
    

    RewardsDistributionRecipient.sol

    RewardsDistributionRecipient.sol 类似Ownable合约,rewardsDistribution是管理员地址,还有一个modifier:onlyRewardsDistribution和onlyOwner()一样的功能。notifyRewardAmount是一个抽象函数,StakingRewards合约继承了该合约。

    pragma solidity ^0.5.16;
    
    contract RewardsDistributionRecipient {
        address public rewardsDistribution;
    
        function notifyRewardAmount(uint256 reward) external;
    
        modifier onlyRewardsDistribution() {
            require(msg.sender == rewardsDistribution, "Caller is not RewardsDistribution contract");
            _;
        }
    }
    

    StakingRewardsFactory.sol

    工厂合约里定义了四个变量:

    • rewardsToken:用作奖励的代币,其实就是 UNI 代币,也可以改成其他erc20代币,但是uni前端代码需要修改
    • stakingRewardsGenesis:质押挖矿开始的时间
    • stakingTokens:用来质押的代币数组,一般就是各交易对的 LPToken
    • stakingRewardsInfoByStakingToken:一个 mapping,用来保存质押代币和质押合约信息之间的映射

    质押合约信息则是一个数据结构:

        struct StakingRewardsInfo {
            address stakingRewards;  // 质押合约地址
            uint rewardAmount;       // 质押合约每周期的奖励总量
        }
    

    rewardsToken 和 stakingRewardsGenesis 在工厂合约的构造函数里就初始化的。除了构造函数,工厂合约还有三个函数:

    • deploy:部署StakingRewards合约的函数
    • notifyRewardAmounts: 将用来挖矿的代币转入到质押合约中,并启动质押挖矿
    • notifyRewardAmount:

    deploy

        function deploy(address stakingToken, uint rewardAmount) public onlyOwner {
            StakingRewardsInfo storage info = stakingRewardsInfoByStakingToken[stakingToken];
            require(info.stakingRewards == address(0), 'StakingRewardsFactory::deploy: already deployed');
    
            info.stakingRewards = address(new StakingRewards(/*_rewardsDistribution=*/ address(this), rewardsToken, stakingToken));
            info.rewardAmount = rewardAmount;
            stakingTokens.push(stakingToken);
        }
    

    两个入参,stakingToken 就是质押代币,在Uniswap中为 LPToken,在自己的Dapp中可以改成erc20 token。rewardAmount 则是奖励数量。

    notifyRewardAmount

    将用来挖矿的代币转入到质押合约中, 前提是需要先将用来挖矿奖励的 UNI 代币数量先转入该工厂合约。有个这个前提,工厂合约的该函数才能实现将 UNI 代币下发到质押合约中去。

        function notifyRewardAmount(address stakingToken) public {
            require(block.timestamp >= stakingRewardsGenesis, 'StakingRewardsFactory::notifyRewardAmount: not ready');
    
            StakingRewardsInfo storage info = stakingRewardsInfoByStakingToken[stakingToken];
            require(info.stakingRewards != address(0), 'StakingRewardsFactory::notifyRewardAmount: not deployed');
    
            if (info.rewardAmount > 0) {
                uint rewardAmount = info.rewardAmount;
                info.rewardAmount = 0;
    
                require(
                    IERC20(rewardsToken).transfer(info.stakingRewards, rewardAmount),
                    'StakingRewardsFactory::notifyRewardAmount: transfer failed'
                );
                StakingRewards(info.stakingRewards).notifyRewardAmount(rewardAmount);
            }
        }
    

    代码逻辑比较简单,

    1. 是判断当前区块的时间需大于等于质押挖矿的开始时间;
    2. 读取出指定的质押代币 stakingToken 映射的质押合约 info,要求 info 的质押合约地址不能为零地址,否则说明还没部署。
    3. 判断 info.rewardAmount 是否大于零,如果为零也不用下发奖励。
      if 语句里面的逻辑主要就是调用 rewardsToken 的 transfer 函数将奖励代币转发给质押合约,再调用质押合约的 notifyRewardAmount 函数触发其内部处理逻辑。另外,将 info.rewardAmount 重置为 0,可以避免向质押合约重复下发奖励代币。

    notifyRewardAmounts

    notifyRewardAmounts 函数,遍历整个质押代币数组,对每个代币再调用 notifyRewardAmount,实现逻辑非常简单。

    StakingRewards.sol

    StakingRewards 合约继承 RewardsDistributionRecipient 合约和 IStakingRewards 接口。

    StakingRewards 存储的变量比较多,除了继承自 RewardsDistributionRecipient 抽象合约里的 rewardsDistribution 变量之外,还有 11 个变量:

    • rewardsToken:奖励代币,即 UNI 代币
    • stakingToken:质押代币,即 LPToken
    • periodFinish:质押挖矿结束的时间,默认时为 0
    • rewardRate:挖矿速率,即每秒挖矿奖励的数量
    • rewardsDuration:挖矿时长,默认设置为 60 天
    • lastUpdateTime:最近一次更新时间
    • rewardPerTokenStored:每单位 token 奖励数量
    • userRewardPerTokenPaid:用户的每单位 token 奖励数量
    • rewards:用户的奖励数量
    • _totalSupply:私有变量,总质押量
    • _balances:私有变量,用户质押余额

    首先看一下notifyRewardAmount函数,该函数由工厂合约触发执行,而且根据工厂合约的代码逻辑,该函数也只会被触发一次。

        function notifyRewardAmount(uint256 reward) external onlyRewardsDistribution updateReward(address(0)) {
            if (block.timestamp >= periodFinish) {
                rewardRate = reward.div(rewardsDuration);
            } else {
                uint256 remaining = periodFinish.sub(block.timestamp);
                uint256 leftover = remaining.mul(rewardRate);
                rewardRate = reward.add(leftover).div(rewardsDuration);
            }
    
            uint balance = rewardsToken.balanceOf(address(this));
            require(rewardRate <= balance.div(rewardsDuration), "Provided reward too high");
    
            lastUpdateTime = block.timestamp;
            periodFinish = block.timestamp.add(rewardsDuration);
            emit RewardAdded(reward);
        }
    

    由于 periodFinish 默认值为 0 且只会在该函数中更新值,所以只会执行 block.timestamp >= periodFinish 的分支逻辑,将从工厂合约转过来的挖矿奖励总量除以挖矿奖励时长,得到挖矿速率 rewardRate,即每秒的挖矿数量。else 分支理论上是执行不到的,但是如果项目方在一个奖励周期中增加了奖励数量,也是可能的。之后,读取 balance 并校验下 rewardRate,可以保证收取到的挖矿奖励余额也是充足的,rewardRate 就不会虚高。最后,更新 lastUpdateTime 和 periodFinish。periodFinish 就是在当前区块时间上加上挖矿时长,就得到了挖矿结束的时间。

    接着,再来看看几个核心业务函数的实现,包括 stake、withdraw、getReward。

    stake 就是质押代币的函数,实现代码如下

        function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
            require(amount > 0, "Cannot stake 0");
            _totalSupply = _totalSupply.add(amount);
            _balances[msg.sender] = _balances[msg.sender].add(amount);
            stakingToken.safeTransferFrom(msg.sender, address(this), amount);
            emit Staked(msg.sender, amount);
        }
    

    函数体内的代码逻辑很简单,将用户指定的质押量 amount 增加到 _totalSupply(总质押量)和 _balances(用户的质押余额),最后调用 stakingToken 的 safeTransferFrom 将代币从用户地址转入当前合约地址。

    withdraw 则是用来提取质押代币的,代码实现也同样很简单,_totalSupply 和 _balances 都减掉提取数量,且将代币从当前合约地址转到用户地址:

        function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
            require(amount > 0, "Cannot withdraw 0");
            _totalSupply = _totalSupply.sub(amount);
            _balances[msg.sender] = _balances[msg.sender].sub(amount);
            stakingToken.safeTransfer(msg.sender, amount);
            emit Withdrawn(msg.sender, amount);
        }
    

    getReward 是领取挖矿奖励的函数,内部逻辑主要就是从 rewards 中读取出用户有多少奖励并清零和转账给到用户:

        function getReward() public nonReentrant updateReward(msg.sender) {
            uint256 reward = rewards[msg.sender];
            if (reward > 0) {
                rewards[msg.sender] = 0;
                rewardsToken.safeTransfer(msg.sender, reward);
                emit RewardPaid(msg.sender, reward);
            }
        }
    

    这几个核心业务函数体内的逻辑都非常好理解,值得一说的其实是每个函数声明最后的 updateReward(msg.sender),这是一个更新挖矿奖励的 modifer,我们来看其代码:

        modifier updateReward(address account) {
            rewardPerTokenStored = rewardPerToken();
            lastUpdateTime = lastTimeRewardApplicable();
            if (account != address(0)) {
                rewards[account] = earned(account);
                userRewardPerTokenPaid[account] = rewardPerTokenStored;
            }
            _;
        }
    

    主要逻辑就是更新几个字段,包括 rewardPerTokenStored、lastUpdateTime 和用户的奖励相关的 rewards[account] 和 userRewardPerTokenPaid[account]。

    其中,还调用到其他三个函数:rewardPerToken()、lastTimeRewardApplicable()、earned(account)。先来看看这三个函数的实现。最简单的就是 lastTimeRewardApplicable:

        function lastTimeRewardApplicable() public view returns (uint256) {
            return Math.min(block.timestamp, periodFinish);
        }
    

    其逻辑就是从当前区块时间和挖矿结束时间两者中返回最小值。因此,当挖矿未结束时返回的就是当前区块时间,而挖矿结束后则返回挖矿结束时间。也因此,挖矿结束后,lastUpdateTime 也会一直等于挖矿结束时间,这点很关键。

    rewardPerToken 函数则是获取每单位质押代币的奖励数量,其实现代码如下:

        function rewardPerToken() public view returns (uint256) {
            if (_totalSupply == 0) {
                return rewardPerTokenStored;
            }
            return
                rewardPerTokenStored.add(
                    lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate).mul(1e18).div(_totalSupply)
                );
        }
    

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

    earned 函数则是计算用户当前的挖矿奖励,代码实现也只有一行代码:

        function earned(address account) public view returns (uint256) {
            return _balances[account].mul(rewardPerToken().sub(userRewardPerTokenPaid[account])).div(1e18).add(rewards[account]);
        }
    

    其逻辑也是计算出增量的每单位质押代币的挖矿奖励,再乘以用户的质押余额得到增量的总挖矿奖励,再加上之前已存储的挖矿奖励,就得到当前总的挖矿奖励。

    至此,StakingRewards 合约的主要实现逻辑也都讲解完了。

    如果只是想部署一个质押挖矿合约,可以不使用挖矿工厂的合约,直接部署StakingReward合约。以下是单独部署一个质押挖矿合约的一个例子

    部署一个质押挖坑合约

    部署合约

    前提,部署、调通unsiwap,创建了交易对。。。

    复制eth mainnet的lp staker合约:https://etherscan.io/address/0xa1484C3aa22a66C62b77E0AE78E15258bd0cB711#code 可以做修改,使用remix部署,将address _rewardsDistribution,address _rewardsToken,address _stakingToken准备好,部署好以后获得合约地址0xdcf71E0741bdfFB3cAcd1dA5FdC86314F490c853,然后去修改uniswap-interface的源码,增加STAKING_REWARDS_INFO.

    给StakingRewards 转uni代币

    使用已有uni的account向0xdcf71E0741bdfFB3cAcd1dA5FdC86314F490c853转uni。让StakingRewards合约可以分发奖励,合约的uni数量需要>=分法的数量。

    激活StakingRewards合约,开始挖矿

    remix 执行notifyRewardAmount方法,入参reward为总的发放的uni数量。
    一开始periodFinish = 0 ,就是启动挖矿,开始时间为block.timestamp
    该方法后续也可以增加奖励uni的额度。

    修改前端代码 uniswap-interface

    path: /Users/walker/Desktop/uniswap/web-3.0.0/src/state/stake

    修改挖矿开始时间, 周期

    export const STAKING_GENESIS = 1631203200 //挖矿开始时间
    export const REWARDS_DURATION_DAYS = 60 //挖矿周期 
    

    添加、修改挖矿池地址

    // TODO add staking rewards addresses here
    export const STAKING_REWARDS_INFO: {
      [chainId in ChainId]?: {
        tokens: [Token, Token]
        stakingRewardAddress: string
      }[]
    } = {
      [ChainId.MAINNET]: [
        {
          tokens: [WETH[ChainId.MAINNET], DAI],
          stakingRewardAddress: '0xa1484C3aa22a66C62b77E0AE78E15258bd0cB711'
        },
        {
          tokens: [WETH[ChainId.MAINNET], USDC],
          stakingRewardAddress: '0x7FBa4B8Dc5E7616e59622806932DBea72537A56b'
        },
        {
          tokens: [WETH[ChainId.MAINNET], USDT],
          stakingRewardAddress: '0x6C3e4cb2E96B01F4b866965A91ed4437839A121a'
        },
        {
          tokens: [WETH[ChainId.MAINNET], WBTC],
          stakingRewardAddress: '0xCA35e32e7926b96A9988f61d510E038108d8068e'
        }
      ],
    //新增,或者修改
      [ChainId.ROPSTEN]: [
        {
          tokens: [WETH[ChainId.ROPSTEN], DAI], //交易对
          stakingRewardAddress:  '0xdcf71E0741bdfFB3cAcd1dA5FdC86314F490c853' //抵押挖矿合约地址
        }
      ]
    }
    

    我使用的是ethereum的ROPSTEN测试链,按照实际修改好。

    重新部署前端,就可以开始抵押挖矿了。

    参考:
    剖析DeFi交易产品之Uniswap:V2下篇

    相关文章

      网友评论

          本文标题:抵押挖矿合约 Uniswap StakingReward

          本文链接:https://www.haomeiwen.com/subject/zzwzwltx.html