Pancake Pool V2 版 Solidity 源码 SmartChef 解读

前置介绍

在 bscscan 上的名字是 SmartChefFactory

本次针对的是 0xFfF5812C35eC100dF51D5C9842e8cC3fe60f9ad7 这个最新版本的合约。

我的社交媒体

基本介绍

分为两个合约

  • SmartChefInitializable
  • SmartChefFactory

其中核心是 SmartChefInitializable 这个合约,而 SmartChefFactory 从名字上也可以看出来是 SmartChefInitializable 的包装。本文重点解读 SmartChefInitializable

数据的结构体

struct UserInfo {
    uint256 amount;     // 用户提供了多少抵押代币
    uint256 rewardDebt; // 奖励债务
}

mapping(address => UserInfo) public userInfo; // 质押代币的用户的信息

这里的 rewardDebt 和 MarstChef 内一样,rewardDebt 这里做一些复杂的数学运算。保证任何时间点,等待分发给用户奖励币数量是:待发奖励 pending = (user.amount * pool.accTokenPerShare) - user.rewardDebt

rewardDebt 需要明确是"用户的奖励债务"。比如用户存款 20,取款 5;此时计算收益的时候会按照 20 进行发放。用户离开了 5,还剩下 15,下次再发奖励的时候,需要剪掉之前已经发掉的 15 所属奖励,这部分挂在账上就是 rewardDebt 记录的就是 15 的奖励部分。在以后再计算的时候,一定要记得减去这部分债务。同理,如果用户此时取款时 20,而不是 5,则债务为 0;因为用户已经没有剩余金额没有取出了。

状态变量

池子的作用是为了存 Swap 平台币,得合作项目方的币,一般都是有时效性的。比如A Token 项目方,和 Pancake 合约,存CAKEA Token,从 12 月 1 日开始,挖到 12 月 30 日;拿出 1 万枚 Token 用于奖励。

所以代码里会有质押的 Token奖励的 Token挖矿的开始区块挖矿的结束区块,以及每块的奖励,这样就可以实现用户的基本需求了;为了辅助计算用户的具体收益,所以还有 每 wei 金额对应的全局收益 以及 最后一次更新池子的区块号。所以会有下面的基本状态变量。

stakedToken;      // 质押的 Token
rewardToken;      // 奖励的 Token
startBlock;       // 挖矿的开始区块
bonusEndBlock;    // 挖矿的结束区块
rewardPerBlock;   // 每块奖励

lastRewardBlock;      // 最后一次池更新的块号
accTokenPerShare;     // 每 wei 金额对应的全局收益

Pancake pool 还可以提供了限制参与抢头矿的方案。是限制地址参与的金额,这样一个地址就不能填入大资金,通过高占比来获取高占比的收益了。比如限制 1 个地址只能存 1 个 Token 进行挖矿,到之后某个区块结束限制。如果大户想参与挖矿,限制结束前,只能把手里十万个币,拆成十万个地址进行参与。这样可以增加散户的积极性。实现这个方案,需要有的属性是 当前池是否限制参与金额。如果限制会有:单个地址的限制额度限制的结束区块

userLimit;          // 是否设置 用户投入金额限制
poolLimitPerUser;   // 限制单个用户的总存款数量(如果不限制用户存款则为 0)
numberBlocksForUserLimit; // 限制用户投入金额的结束区块(需要大于开始区块)

是否需要持有 Pancake NFT,如果需要,门槛点数是多少,所以有下面的属性

pancakeProfile;
pancakeProfileIsRequested; // 是否需要 Pancake Profile
pancakeProfileThresholdPoints; // Pancake Profile 门槛

下面是相关的配套数据

isInitialized; // 是否初始化
SMART_CHEF_FACTORY;
PRECISION_FACTOR; // 精度系数

构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
constructor(
    address _pancakeProfile,
    bool _pancakeProfileIsRequested,
    uint256 _pancakeProfileThresholdPoints
) {
    SMART_CHEF_FACTORY = msg.sender; // 设置 SMART_CHEF_FACTORY 地址
    IPancakeProfile(_pancakeProfile).getTeamProfile(1); // 验证地址
    pancakeProfile = IPancakeProfile(_pancakeProfile);
    pancakeProfileIsRequested = _pancakeProfileIsRequested;
    pancakeProfileThresholdPoints = _pancakeProfileThresholdPoints;
}

SMART_CHEF_FACTORYSmartChefInitializable 的包装调用合约。这里主要是做 NFT 资产的判断

方法

  • initialize:初始化
  • deposit:质押
  • withdraw:赎回
  • stopReward: 停止奖励
  • updatePoolLimitPerUser: 更新每个用户的池限制
  • updateRewardPerBlock: 更新每块的产出奖励
  • updateStartAndEndBlocks: 更新开始和结束区块
  • updateProfileAndThresholdPointsRequirement 更新配置文件和阈值点要求
  • pendingReward: 待定奖励

这里和 MarstChef 大同小异的,主要看下 initialize 和 deposit 的方法

initialize

function initialize(
    IERC20Metadata _stakedToken,
    IERC20Metadata _rewardToken,
    uint256 _rewardPerBlock,
    uint256 _startBlock,
    uint256 _bonusEndBlock,
    uint256 _poolLimitPerUser, // 用户限制
    uint256 _numberBlocksForUserLimit,
    address _admin
) external {
    require(!isInitialized, "Already initialized");
    require(msg.sender == SMART_CHEF_FACTORY, "Not factory");

    isInitialized = true; // 初始化这个合约

    // 基础信息
    stakedToken = _stakedToken;
    rewardToken = _rewardToken;
    rewardPerBlock = _rewardPerBlock;
    startBlock = _startBlock;
    bonusEndBlock = _bonusEndBlock;

    // 如果做限制
    if (_poolLimitPerUser > 0) {
        userLimit = true;
        poolLimitPerUser = _poolLimitPerUser;
        numberBlocksForUserLimit = _numberBlocksForUserLimit;
    }

    uint256 decimalsRewardToken = uint256(rewardToken.decimals());
    require(decimalsRewardToken < 30, "Must be inferior to 30");

    PRECISION_FACTOR = uint256(10**(uint256(30) - decimalsRewardToken));

    // Set the lastRewardBlock as the startBlock
    lastRewardBlock = startBlock;

    // 转移 admin 地址
    transferOwnership(_admin);
}

deposit

function deposit(uint256 _amount) external nonReentrant {
    UserInfo storage user = userInfo[msg.sender];

    // 检查用户是否有一个活动的配置文件
    // 如果 pancakeProfileIsRequested 为 false,并且 pancakeProfileThresholdPoints 为0,无NFT要求,通过判断
    // 如果有NFT要求,则 user.isActive 必须为true才可以
    require(
        (!pancakeProfileIsRequested && pancakeProfileThresholdPoints == 0) ||
            pancakeProfile.getUserStatus(msg.sender),
        "Deposit: Must have an active profile"
    );

    uint256 numberUserPoints = 0; // 用户点数

    // getUserProfile返回值是: userId / numberPoints / teamId / nftAddress / tokenId / isActive
    if (pancakeProfileThresholdPoints > 0) {
        (, numberUserPoints, , , , ) = pancakeProfile.getUserProfile(msg.sender);
    }

    // 如果要求的点数不为0,则需要判断用户的NFT点数是否满足
    require(
        pancakeProfileThresholdPoints == 0 || numberUserPoints >= pancakeProfileThresholdPoints,
        "Deposit: User is not get enough user points"
    );

    // hasUserLimit中 如果 userLimit 为 false,则返回false。
    // 如果当前区块已经大于 开始区块+限制区块,表示已经过了限制期,返回false。
    // 如果以上两个都不是,则返回true,代表现在还是限制中
    userLimit = hasUserLimit();

    // 如果有限制,用户存款不能超过限制
    require(!userLimit || ((_amount + user.amount) <= poolLimitPerUser), "Deposit: Amount above limit");


    // 下面的逻辑和 MarstChef 完全相同了。
    // 更新池
    _updatePool();

    // 如果有奖励,则发掉
    if (user.amount > 0) {
        uint256 pending = (user.amount * accTokenPerShare) / PRECISION_FACTOR - user.rewardDebt;
        if (pending > 0) {
            rewardToken.safeTransfer(address(msg.sender), pending);
        }
    }

    // 如果有存款,则转账
    if (_amount > 0) {
        user.amount = user.amount + _amount;
        stakedToken.safeTransferFrom(address(msg.sender), address(this), _amount);
    }


    // 写用户债务
    user.rewardDebt = (user.amount * accTokenPerShare) / PRECISION_FACTOR;

    emit Deposit(msg.sender, _amount);
}

冗余处理

  • emergencyWithdraw: 紧急取款
  • emergencyRewardWithdraw 紧急取奖励
  • recoverToken: 取出用户转入的错误 Token

辅助方法

  • _updatePool
  • _getMultiplier
  • hasUserLimit

事件相关

每次修改状态变量都触发事件

event Deposit(address indexed user, uint256 amount);
event EmergencyWithdraw(address indexed user, uint256 amount);
event NewStartAndEndBlocks(uint256 startBlock, uint256 endBlock);
event NewRewardPerBlock(uint256 rewardPerBlock);
event NewPoolLimit(uint256 poolLimitPerUser);
event RewardsStop(uint256 blockNumber);
event TokenRecovery(address indexed token, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event UpdateProfileAndThresholdPointsRequirement(bool isProfileRequested, uint256 thresholdPoints);

相关的扩展

关于站长

我叫朱安邦,本站的站长。如果您对网站有什么好的建议,欢迎在 Twitter 上与我交流