前置介绍
在 bscscan 上的名字是 SmartChefFactory。
- 文档: https://docs.pancakeswap.finance/code/smart-contracts/syrup-pools
- Github 地址: https://github.com/pancakeswap/pancake-smart-contracts/tree/master/projects/smartchef/v2/contracts
- BscScan 地址:
- 0x927158Be21Fe3D4da7E96931bb27Fd5059A8CbC2 May-05-2021 部署
- 0xe2aECF96D23575b11624d0891C0828E767c8cb8B Jan-04-2022 部署
- 0xFfF5812C35eC100dF51D5C9842e8cC3fe60f9ad7 Jan-25-2022 部署 (最新)
本次针对的是 0xFfF5812C35eC100dF51D5C9842e8cC3fe60f9ad7
这个最新版本的合约。
我的社交媒体
- 关注我的推特:@anbang_account
- 加入合约交流群:Solidity 智能合约交流群 (Discord)
基本介绍
分为两个合约
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 合约,存CAKE
挖A 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; // 精度系数
构造函数
|
|
SMART_CHEF_FACTORY
是 SmartChefInitializable
的包装调用合约。这里主要是做 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);